package com.oracle.svm.hosted.classinitialization;
import static com.oracle.svm.core.SubstrateOptions.TraceClassInitialization;
import static com.oracle.svm.core.SubstrateOptions.TraceObjectInstantiation;
import java.lang.reflect.Proxy;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.graalvm.compiler.serviceprovider.GraalUnsafeAccess;
import com.oracle.graal.pointsto.constraints.UnsupportedFeatures;
import com.oracle.graal.pointsto.infrastructure.OriginalClassProvider;
import com.oracle.graal.pointsto.reports.ReportUtils;
import com.oracle.svm.core.SubstrateOptions;
import com.oracle.svm.core.option.SubstrateOptionsParser;
import com.oracle.svm.core.util.UserError;
import com.oracle.svm.hosted.ImageClassLoader;
import com.oracle.svm.hosted.NativeImageOptions;
import com.oracle.svm.hosted.c.GraalAccess;
import jdk.vm.ci.meta.MetaAccessProvider;
import jdk.vm.ci.meta.ResolvedJavaType;
import sun.misc.Unsafe;
public class ConfigurableClassInitialization implements ClassInitializationSupport {
private static final Unsafe UNSAFE = GraalUnsafeAccess.getUnsafe();
private final ClassInitializationConfiguration classInitializationConfiguration = new ClassInitializationConfiguration();
private final Map<Class<?>, InitKind> classInitKinds = new ConcurrentHashMap<>();
private static final Map<Class<?>, StackTraceElement[]> initializedClasses = new ConcurrentHashMap<>();
private static final Map<Object, StackTraceElement[]> instantiatedObjects = new ConcurrentHashMap<>();
private boolean configurationSealed;
private final ImageClassLoader loader;
private UnsupportedFeatures unsupportedFeatures;
protected MetaAccessProvider metaAccess;
static EarlyClassInitializerAnalysis earlyClassInitializerAnalysis = new EarlyClassInitializerAnalysis();
public ConfigurableClassInitialization(MetaAccessProvider metaAccess, ImageClassLoader loader) {
this.metaAccess = metaAccess;
this.loader = loader;
}
@Override
public void setConfigurationSealed(boolean sealed) {
configurationSealed = sealed;
if (configurationSealed && ClassInitializationFeature.Options.PrintClassInitialization.getValue()) {
List<ClassOrPackageConfig> allConfigs = classInitializationConfiguration.allConfigs();
allConfigs.sort(Comparator.comparing(ClassOrPackageConfig::getName));
String path = Paths.get(Paths.get(SubstrateOptions.Path.getValue()).toString(), "reports").toAbsolutePath().toString();
ReportUtils.report("initializer configuration", path, "initializer_configuration", "txt", writer -> {
for (ClassOrPackageConfig config : allConfigs) {
writer.append(config.getName()).append(" -> ").append(config.getKind().toString()).append(" reasons: ")
.append(String.join(" and ", config.getReasons())).append(System.lineSeparator());
}
});
}
}
@Override
public void setUnsupportedFeatures(UnsupportedFeatures unsupportedFeatures) {
this.unsupportedFeatures = unsupportedFeatures;
}
private InitKind computeInitKindAndMaybeInitializeClass(Class<?> clazz) {
return computeInitKindAndMaybeInitializeClass(clazz, true);
}
@Override
public InitKind specifiedInitKindFor(Class<?> clazz) {
return classInitializationConfiguration.lookupKind(clazz.getTypeName()).getLeft();
}
@Override
public boolean canBeProvenSafe(Class<?> clazz) {
InitKind initKind = specifiedInitKindFor(clazz);
return initKind == null || (initKind.isRunTime() && !isStrictlyDefined(clazz));
}
private Boolean isStrictlyDefined(Class<?> clazz) {
return classInitializationConfiguration.lookupKind(clazz.getTypeName()).getRight();
}
@Override
public Set<Class<?>> classesWithKind(InitKind kind) {
return classInitKinds.entrySet().stream()
.filter(e -> e.getValue() == kind)
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
@Override
public boolean shouldInitializeAtRuntime(ResolvedJavaType type) {
return computeInitKindAndMaybeInitializeClass(getJavaClass(type)) != InitKind.BUILD_TIME;
}
@Override
public boolean shouldInitializeAtRuntime(Class<?> clazz) {
return computeInitKindAndMaybeInitializeClass(clazz) != InitKind.BUILD_TIME;
}
@Override
public void maybeInitializeHosted(ResolvedJavaType type) {
computeInitKindAndMaybeInitializeClass(getJavaClass(type));
}
private InitKind ensureClassInitialized(Class<?> clazz, boolean allowErrors) {
try {
UNSAFE.ensureClassInitialized(clazz);
return InitKind.BUILD_TIME;
} catch (NoClassDefFoundError ex) {
if (NativeImageOptions.AllowIncompleteClasspath.getValue()) {
if (!allowErrors) {
System.out.println("Warning: class initialization of class " + clazz.getTypeName() + " failed with exception " +
ex.getClass().getTypeName() + (ex.getMessage() == null ? "" : ": " + ex.getMessage()) + ". This class will be initialized at run time because option " +
SubstrateOptionsParser.commandArgument(NativeImageOptions.AllowIncompleteClasspath, "+") + " is used for image building. " +
instructionsToInitializeAtRuntime(clazz));
}
return InitKind.RUN_TIME;
} else {
return reportInitializationError(allowErrors, clazz, ex);
}
} catch (Throwable t) {
return reportInitializationError(allowErrors, clazz, t);
}
}
private InitKind reportInitializationError(boolean allowErrors, Class<?> clazz, Throwable t) {
if (allowErrors) {
return InitKind.RUN_TIME;
} else {
String msg = String.format("Class initialization of %s failed. %s", clazz.getTypeName(), instructionsToInitializeAtRuntime(clazz));
if (unsupportedFeatures != null) {
unsupportedFeatures.addMessage(clazz.getTypeName(), null, msg, null, t);
return InitKind.RUN_TIME;
} else {
throw UserError.abort(t, "%s", msg);
}
}
}
private static String instructionsToInitializeAtRuntime(Class<?> clazz) {
return "Use the option " + SubstrateOptionsParser.commandArgument(ClassInitializationFeature.Options.ClassInitialization, clazz.getTypeName(), "initialize-at-run-time") +
" to explicitly request delayed initialization of this class.";
}
private static Class<?> getJavaClass(ResolvedJavaType type) {
return OriginalClassProvider.getJavaClass(GraalAccess.getOriginalSnippetReflection(), type);
}
@Override
public void initializeAtRunTime(String name, String reason) {
UserError.guarantee(!configurationSealed, "The class initialization configuration can be changed only before the phase analysis.");
Class<?> clazz = loader.findClass(name).get();
if (clazz != null) {
classInitializationConfiguration.insert(name, InitKind.RUN_TIME, reason, true);
initializeAtRunTime(clazz, reason);
} else {
classInitializationConfiguration.insert(name, InitKind.RUN_TIME, reason, false);
}
}
@Override
public void initializeAtBuildTime(String name, String reason) {
UserError.guarantee(!configurationSealed, "The class initialization configuration can be changed only before the phase analysis.");
Class<?> clazz = loader.findClass(name).get();
if (clazz != null) {
classInitializationConfiguration.insert(name, InitKind.BUILD_TIME, reason, true);
initializeAtBuildTime(clazz, reason);
} else {
classInitializationConfiguration.insert(name, InitKind.BUILD_TIME, reason, false);
}
}
@Override
public void rerunInitialization(String name, String reason) {
UserError.guarantee(!configurationSealed, "The class initialization configuration can be changed only before the phase analysis.");
Class<?> clazz = loader.findClass(name).get();
if (clazz != null) {
classInitializationConfiguration.insert(name, InitKind.RERUN, reason, true);
rerunInitialization(clazz, reason);
} else {
classInitializationConfiguration.insert(name, InitKind.RERUN, reason, false);
}
}
@Override
public void initializeAtRunTime(Class<?> clazz, String reason) {
UserError.guarantee(!configurationSealed, "The class initialization configuration can be changed only before the phase analysis.");
classInitializationConfiguration.insert(clazz.getTypeName(), InitKind.RUN_TIME, reason, true);
setSubclassesAsRunTime(clazz);
checkEagerInitialization(clazz);
if (!UNSAFE.shouldBeInitialized(clazz)) {
throw UserError.abort("The class %1$s has already been initialized; it is too late to register %1$s for build-time initialization (%2$s). %3$s",
clazz.getTypeName(), reason,
classInitializationErrorMessage(clazz, "Try avoiding this conflict by avoiding to initialize the class that caused initialization of " + clazz.getTypeName() +
" or by not marking " + clazz.getTypeName() + " for build-time initialization."));
}
computeInitKindAndMaybeInitializeClass(clazz, false);
InitKind previousKind = classInitKinds.put(clazz, InitKind.RUN_TIME);
if (previousKind == InitKind.BUILD_TIME) {
throw UserError.abort("Class is already initialized, so it is too late to register delaying class initialization: %s for reason: %s", clazz.getTypeName(), reason);
} else if (previousKind == InitKind.RERUN) {
throw UserError.abort("Class is registered both for delaying and rerunning the class initializer: %s for reason: %s", clazz.getTypeName(), reason);
}
}
private static boolean isClassListedInStringOption(String option, Class<?> clazz) {
return Arrays.asList(option.split(",")).contains(clazz.getName());
}
private static boolean isClassInitializationTracked(Class<?> clazz) {
return TraceClassInitialization.hasBeenSet() && isClassListedInStringOption(TraceClassInitialization.getValue(), clazz);
}
private static boolean isObjectInstantiationForClassTracked(Class<?> clazz) {
return TraceObjectInstantiation.hasBeenSet() && isClassListedInStringOption(TraceObjectInstantiation.getValue(), clazz);
}
private static String classInitializationErrorMessage(Class<?> clazz, String action) {
if (!isClassInitializationTracked(clazz)) {
return "To see why " + clazz.getName() + " got initialized use " + SubstrateOptionsParser.commandArgument(TraceClassInitialization, clazz.getName());
} else if (initializedClasses.containsKey(clazz)) {
StackTraceElement[] trace = initializedClasses.get(clazz);
String culprit = null;
boolean containsLambdaMetaFactory = false;
for (StackTraceElement stackTraceElement : trace) {
if (stackTraceElement.getMethodName().equals("<clinit>")) {
culprit = stackTraceElement.getClassName();
}
if (stackTraceElement.getClassName().equals("java.lang.invoke.LambdaMetafactory")) {
containsLambdaMetaFactory = true;
}
}
if (containsLambdaMetaFactory) {
return clazz.getTypeName() + " was initialized through a lambda (https://github.com/oracle/graal/issues/1218). Try marking " + clazz.getTypeName() +
" for build-time initialization with " + SubstrateOptionsParser.commandArgument(
ClassInitializationFeature.Options.ClassInitialization, clazz.getTypeName(), "initialize-at-build-time") +
".";
} else if (culprit != null) {
return culprit + " caused initialization of this class with the following trace: \n" + classInitializationTrace(clazz);
} else {
return clazz.getTypeName() + " has been initialized through the following trace:\n" + classInitializationTrace(clazz);
}
} else {
return clazz.getTypeName() + " has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked. " + action;
}
}
@Override
public String objectInstantiationTraceMessage(Object obj, String action) {
if (!isObjectInstantiationForClassTracked(obj.getClass())) {
return " To see how this object got instantiated use " + SubstrateOptionsParser.commandArgument(TraceObjectInstantiation, obj.getClass().getName()) + ".";
} else if (instantiatedObjects.containsKey(obj)) {
String culprit = null;
StackTraceElement[] trace = instantiatedObjects.get(obj);
boolean containsLambdaMetaFactory = false;
for (StackTraceElement stackTraceElement : trace) {
if (stackTraceElement.getMethodName().equals("<clinit>")) {
culprit = stackTraceElement.getClassName();
}
if (stackTraceElement.getClassName().equals("java.lang.invoke.LambdaMetafactory")) {
containsLambdaMetaFactory = true;
}
}
if (containsLambdaMetaFactory) {
return " Object was instantiated through a lambda (https://github.com/oracle/graal/issues/1218). Try marking " + obj.getClass().getTypeName() +
" for build-time initialization with " + SubstrateOptionsParser.commandArgument(
ClassInitializationFeature.Options.ClassInitialization, obj.getClass().getTypeName(), "initialize-at-build-time") +
".";
} else if (culprit != null) {
return " Object has been initialized by the " + culprit + " class initializer with a trace: \n " + getTraceString(instantiatedObjects.get(obj)) + ". " + action;
} else {
return " Object has been initialized through the following trace:\n" + getTraceString(instantiatedObjects.get(obj)) + ". " + action;
}
} else {
return " Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.";
}
}
private static String classInitializationTrace(Class<?> clazz) {
return getTraceString(initializedClasses.get(clazz));
}
private static String getTraceString(StackTraceElement[] trace) {
StringBuilder b = new StringBuilder();
for (int i = 0; i < trace.length; i++) {
StackTraceElement stackTraceElement = trace[i];
b.append("\tat ").append(stackTraceElement.toString()).append("\n");
}
return b.toString();
}
@Override
public void rerunInitialization(Class<?> clazz, String reason) {
UserError.guarantee(!configurationSealed, "The class initialization configuration can be changed only before the phase analysis.");
classInitializationConfiguration.insert(clazz.getTypeName(), InitKind.RERUN, reason, true);
checkEagerInitialization(clazz);
try {
UNSAFE.ensureClassInitialized(clazz);
} catch (Throwable ex) {
throw UserError.abort(ex, "Class initialization failed for %s. The class is requested for re-running (reason: %s)", clazz.getTypeName(), reason);
}
computeInitKindAndMaybeInitializeClass(clazz, false);
InitKind previousKind = classInitKinds.put(clazz, InitKind.RERUN);
if (previousKind != null) {
if (previousKind == InitKind.BUILD_TIME) {
throw UserError.abort("The information that the class should be initialized during image building has already been used, " +
"so it is too late to register the class initializer of %s for re-running. The reason for re-run request is %s",
clazz.getTypeName(), reason);
} else if (previousKind.isRunTime()) {
throw UserError.abort("Class or a superclass is already registered for delaying the class initializer, " +
"so it is too late to register the class initializer of %s for re-running. The reason for re-run request is %s",
clazz.getTypeName(), reason);
}
}
}
@Override
public void initializeAtBuildTime(Class<?> aClass, String reason) {
UserError.guarantee(!configurationSealed, "The class initialization configuration can be changed only before the phase analysis.");
classInitializationConfiguration.insert(aClass.getTypeName(), InitKind.BUILD_TIME, reason, true);
forceInitializeHosted(aClass, reason, false);
}
private void setSubclassesAsRunTime(Class<?> clazz) {
if (clazz.isInterface() && !metaAccess.lookupJavaType(clazz).declaresDefaultMethods()) {
return;
}
loader.findSubclasses(clazz, false).stream()
.filter(c -> !c.equals(clazz))
.filter(c -> !(c.isInterface() && !metaAccess.lookupJavaType(c).declaresDefaultMethods()))
.forEach(c -> classInitializationConfiguration.insert(c.getTypeName(), InitKind.RUN_TIME, "subtype of " + clazz.getTypeName(), true));
}
@Override
public void reportClassInitialized(Class<?> clazz, StackTraceElement[] stackTrace) {
assert TraceClassInitialization.hasBeenSet();
initializedClasses.put(clazz, relevantStackTrace(stackTrace));
}
@Override
public void reportObjectInstantiated(Object o, StackTraceElement[] stackTrace) {
assert TraceObjectInstantiation.hasBeenSet();
instantiatedObjects.putIfAbsent(o, relevantStackTrace(stackTrace));
}
private static StackTraceElement[] relevantStackTrace(StackTraceElement[] stack) {
ArrayList<StackTraceElement> filteredStack = new ArrayList<>();
int lastClinit = 0;
boolean containsLambdaMetaFactory = false;
for (int i = 0; i < stack.length; i++) {
StackTraceElement stackTraceElement = stack[i];
if ("<clinit>".equals(stackTraceElement.getMethodName())) {
lastClinit = i;
}
if (stackTraceElement.getClassName().equals("java.lang.invoke.LambdaMetafactory")) {
containsLambdaMetaFactory = true;
}
filteredStack.add(stackTraceElement);
}
List<StackTraceElement> finalStack = lastClinit != 0 && !containsLambdaMetaFactory ? filteredStack.subList(0, lastClinit + 1) : filteredStack;
return finalStack.toArray(new StackTraceElement[0]);
}
@Override
public void forceInitializeHosted(Class<?> clazz, String reason, boolean allowInitializationErrors) {
if (clazz == null) {
return;
}
classInitializationConfiguration.insert(clazz.getTypeName(), InitKind.BUILD_TIME, reason, true);
InitKind initKind = ensureClassInitialized(clazz, allowInitializationErrors);
classInitKinds.put(clazz, initKind);
forceInitializeHosted(clazz.getSuperclass(), "super type of " + clazz.getTypeName(), allowInitializationErrors);
forceInitializeInterfaces(clazz.getInterfaces(), "super type of " + clazz.getTypeName());
}
private void forceInitializeInterfaces(Class<?>[] interfaces, String reason) {
for (Class<?> iface : interfaces) {
if (metaAccess.lookupJavaType(iface).declaresDefaultMethods()) {
classInitializationConfiguration.insert(iface.getTypeName(), InitKind.BUILD_TIME, reason, true);
ensureClassInitialized(iface, false);
classInitKinds.put(iface, InitKind.BUILD_TIME);
}
forceInitializeInterfaces(iface.getInterfaces(), "super type of " + iface.getTypeName());
}
}
@Override
public boolean checkDelayedInitialization() {
Set<Class<?>> illegalyInitialized = new HashSet<>();
for (Map.Entry<Class<?>, InitKind> entry : classInitKinds.entrySet()) {
if (entry.getValue().isRunTime() && !UNSAFE.shouldBeInitialized(entry.getKey())) {
illegalyInitialized.add(entry.getKey());
}
}
if (illegalyInitialized.size() > 0) {
StringBuilder detailedMessage = new StringBuilder("Classes that should be initialized at run time got initialized during image building:\n ");
illegalyInitialized.forEach(c -> {
InitKind specifiedKind = specifiedInitKindFor(c);
if (specifiedKind == null) {
detailedMessage.append(c.getTypeName()).append(" was unintentionally initialized at build time. ");
detailedMessage.append(classInitializationErrorMessage(c,
"Try marking this class for build-time initialization with " + SubstrateOptionsParser.commandArgument(ClassInitializationFeature.Options.ClassInitialization,
c.getTypeName(), "initialize-at-build-time")))
.append("\n");
} else {
assert specifiedKind.isRunTime() : "Specified kind must be the same as actual kind for type " + c.getTypeName();
String reason = classInitializationConfiguration.lookupReason(c.getTypeName());
detailedMessage.append(c.getTypeName()).append(" the class was requested to be initialized at run time (").append(reason).append("). ")
.append(classInitializationErrorMessage(c, "Try avoiding to initialize the class that caused initialization of " + c.getTypeName()))
.append("\n");
}
});
if (!TraceClassInitialization.hasBeenSet()) {
String traceClassInitArguments = illegalyInitialized.stream().map(Class::getName).collect(Collectors.joining(","));
System.out.println("To see how the classes got initialized, use " + SubstrateOptionsParser.commandArgument(TraceClassInitialization, traceClassInitArguments));
}
throw UserError.abort("%s", detailedMessage);
}
return true;
}
private static void checkEagerInitialization(Class<?> clazz) {
if (clazz.isPrimitive() || clazz.isArray()) {
throw UserError.abort("Primitive types and array classes are initialized eagerly because initialization is side-effect free. " +
"It is not possible (and also not useful) to register them for run time initialization. Culprit: %s", clazz.getTypeName());
}
if (clazz.isAnnotation()) {
throw UserError.abort("Class initialization of annotation classes cannot be delayed to runtime. Culprit: %s", clazz.getTypeName());
}
}
private InitKind computeInitKindAndMaybeInitializeClass(Class<?> clazz, boolean memoize) {
if (classInitKinds.containsKey(clazz)) {
return classInitKinds.get(clazz);
}
if (clazz.isAnnotation()) {
forceInitializeHosted(clazz, "all annotations are initialized", false);
return InitKind.BUILD_TIME;
}
if (clazz.isEnum() && !UNSAFE.shouldBeInitialized(clazz)) {
if (memoize) {
forceInitializeHosted(clazz, "enums referred in annotations must be initialized", false);
}
return InitKind.BUILD_TIME;
}
InitKind clazzResult = computeInitKindForClass(clazz);
InitKind superResult = InitKind.BUILD_TIME;
if (clazz.getSuperclass() != null) {
superResult = superResult.max(computeInitKindAndMaybeInitializeClass(clazz.getSuperclass(), memoize));
}
superResult = superResult.max(processInterfaces(clazz, memoize));
if (memoize && superResult == InitKind.BUILD_TIME && clazzResult == InitKind.RUN_TIME && canBeProvenSafe(clazz)) {
if (earlyClassInitializerAnalysis.canInitializeWithoutSideEffects(clazz)) {
clazzResult = ensureClassInitialized(clazz, true);
}
}
InitKind result = superResult.max(clazzResult);
if (memoize) {
if (!result.isRunTime()) {
result = result.max(ensureClassInitialized(clazz, false));
}
InitKind previous = classInitKinds.put(clazz, result);
assert previous == null || previous == result : "Overwriting existing value: previous " + previous + " new " + result;
}
return result;
}
private InitKind processInterfaces(Class<?> clazz, boolean memoizeEager) {
InitKind result = InitKind.BUILD_TIME;
for (Class<?> iface : clazz.getInterfaces()) {
if (metaAccess.lookupJavaType(iface).declaresDefaultMethods()) {
result = result.max(computeInitKindAndMaybeInitializeClass(iface, memoizeEager));
} else {
result = result.max(processInterfaces(iface, memoizeEager));
}
}
return result;
}
private InitKind computeInitKindForClass(Class<?> clazz) {
if (clazz.isPrimitive() || clazz.isArray()) {
return InitKind.BUILD_TIME;
} else if (clazz.isAnnotation()) {
return InitKind.BUILD_TIME;
} else if (Proxy.isProxyClass(clazz) && isProxyFromAnnotation(clazz)) {
return InitKind.BUILD_TIME;
} else if (clazz.getTypeName().contains("$$StringConcat")) {
return InitKind.BUILD_TIME;
} else if (specifiedInitKindFor(clazz) != null) {
return specifiedInitKindFor(clazz);
} else {
return InitKind.RUN_TIME;
}
}
private static boolean isProxyFromAnnotation(Class<?> clazz) {
for (Class<?> interfaces : clazz.getInterfaces()) {
if (interfaces.isAnnotation()) {
return true;
}
}
return false;
}
}