Skip to content

Commit

Permalink
[GR-50259] Throw exceptions for missing resource bundle registrations
Browse files Browse the repository at this point in the history
PullRequest: graal/16332
  • Loading branch information
loicottet committed Mar 6, 2024
2 parents d2c2364 + e161a35 commit 60e1fdb
Show file tree
Hide file tree
Showing 13 changed files with 244 additions and 110 deletions.
1 change: 1 addition & 0 deletions substratevm/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This changelog summarizes major changes to GraalVM Native Image.
* (GR-49655) Experimental support for parts of the [Foreign Function & Memory API](https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/ForeignInterface.md) (part of "Project Panama", [JEP 454](https://openjdk.org/jeps/454)) on AMD64. Must be enabled with `-H:+ForeignAPISupport` (requiring `-H:+UnlockExperimentalVMOptions`).
* (GR-46407) Correctly rethrow build-time linkage errors at run-time for registered reflection queries.
* (GR-51002) Improve intrinsification of method handles. This especially improves the performance of `equals` and `hashCode` methods for records, which use method handles that are now intrinsified.
* (GR-50529) Native Image now throws a specific error when trying to access unregistered resource bundles instead of failing on a subsequent reflection or resource query.

## GraalVM for JDK 21 (Internal Version 23.1.0)
* (GR-35746) Lower the default aligned chunk size from 1 MB to 512 KB for the serial and epsilon GCs, reducing memory usage and image size in many cases.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -704,33 +704,39 @@ private static boolean getBundleImpl(JNIEnvironment jni, JNIObjectHandle thread,
return true;
}

/*
* Bundles.putBundleInCache is the single point through which all bundles queried through
* sun.util.resources.Bundles go
*/
private static boolean putBundleInCache(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) {
JNIObjectHandle callerClass = state.getDirectCallerClass();
JNIObjectHandle cacheKey = getObjectArgument(thread, 0);
JNIObjectHandle baseName = Support.callObjectMethod(jni, cacheKey, agent.handles().sunUtilResourcesBundlesCacheKeyGetName);
private static String readLocaleTag(JNIEnvironment jni, JNIObjectHandle locale) {
JNIObjectHandle languageTag = Support.callObjectMethod(jni, locale, agent.handles().javaUtilLocaleToLanguageTag);
if (clearException(jni)) {
baseName = nullHandle();
/*- return root locale */
return "";
}
JNIObjectHandle locale = Support.callObjectMethod(jni, cacheKey, agent.handles().sunUtilResourcesBundlesCacheKeyGetLocale);

JNIObjectHandle reconstructedLocale = Support.callStaticObjectMethodL(jni, agent.handles().javaUtilLocale, agent.handles().javaUtilLocaleForLanguageTag, languageTag);
if (clearException(jni)) {
locale = nullHandle();
reconstructedLocale = nullHandle();
}
if (Support.callBooleanMethodL(jni, locale, agent.handles().javaUtilLocaleEquals, reconstructedLocale)) {
return fromJniString(jni, languageTag);
} else {
/*
* Ill-formed locale, we do our best to return a unique description of the locale.
* Locale.toLanguageTag simply ignores ill-formed locale elements, which creates
* confusion when trying to determine whether a specific locale was registered.
*/
String language = getElementString(jni, locale, agent.handles().javaUtilLocaleGetLanguage);
String country = getElementString(jni, locale, agent.handles().javaUtilLocaleGetCountry);
String variant = getElementString(jni, locale, agent.handles().javaUtilLocaleGetVariant);

return String.join("-", language, country, variant);
}
traceReflectBreakpoint(jni, nullHandle(), nullHandle(), callerClass, bp.specification.methodName, true, state.getFullStackTraceOrNull(),
Tracer.UNKNOWN_VALUE, Tracer.UNKNOWN_VALUE, fromJniString(jni, baseName), readLocaleTag(jni, locale), Tracer.UNKNOWN_VALUE);
return true;
}

private static String readLocaleTag(JNIEnvironment jni, JNIObjectHandle locale) {
JNIObjectHandle languageTag = Support.callObjectMethod(jni, locale, agent.handles().getJavaUtilLocaleToLanguageTag(jni));
private static String getElementString(JNIEnvironment jni, JNIObjectHandle object, JNIMethodId getter) {
JNIObjectHandle valueHandle = Support.callObjectMethod(jni, object, getter);
if (clearException(jni)) {
/*- return root locale */
return "";
valueHandle = nullHandle();
}
return fromJniString(jni, languageTag);
return valueHandle.notEqual(nullHandle()) ? fromJniString(jni, valueHandle) : "";
}

private static boolean loadClass(JNIEnvironment jni, JNIObjectHandle thread, Breakpoint bp, InterceptedState state) {
Expand Down Expand Up @@ -1640,8 +1646,6 @@ private interface BreakpointHandler {
"getBundleImpl",
"(Ljava/lang/Module;Ljava/lang/Module;Ljava/lang/String;Ljava/util/Locale;Ljava/util/ResourceBundle$Control;)Ljava/util/ResourceBundle;",
BreakpointInterceptor::getBundleImpl),
brk("sun/util/resources/Bundles", "putBundleInCache", "(Lsun/util/resources/Bundles$CacheKey;Ljava/util/ResourceBundle;)Ljava/util/ResourceBundle;",
BreakpointInterceptor::putBundleInCache),

// In Java 9+, these are Java methods that call private methods
optionalBrk("jdk/internal/misc/Unsafe", "objectFieldOffset", "(Ljava/lang/Class;Ljava/lang/String;)J", BreakpointInterceptor::objectFieldOffsetByName),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,16 @@ public class NativeImageAgentJNIHandleSet extends JNIHandleSet {
private JNIObjectHandle javaLangReflectProxy = WordFactory.nullPointer();
private JNIMethodId javaLangReflectProxyIsProxyClass = WordFactory.nullPointer();

private JNIMethodId javaUtilLocaleToLanguageTag;
private JNIFieldId javaUtilResourceBundleParentField;
private JNIMethodId javaUtilResourceBundleGetLocale;
final JNIObjectHandle javaUtilLocale;
final JNIMethodId javaUtilLocaleGetLanguage;
final JNIMethodId javaUtilLocaleGetCountry;
final JNIMethodId javaUtilLocaleGetVariant;
final JNIMethodId javaUtilLocaleForLanguageTag;
final JNIMethodId javaUtilLocaleToLanguageTag;
final JNIMethodId javaUtilLocaleEquals;

final JNIFieldId javaLangInvokeSerializedLambdaCapturingClass;

final JNIMethodId sunUtilResourcesBundlesCacheKeyGetName;
final JNIMethodId sunUtilResourcesBundlesCacheKeyGetLocale;

final JNIMethodId javaLangModuleGetName;

private JNIMethodId javaLangInvokeCallSiteMakeSite = WordFactory.nullPointer();
Expand Down Expand Up @@ -132,12 +133,16 @@ public class NativeImageAgentJNIHandleSet extends JNIHandleSet {
JNIObjectHandle serializedLambda = findClass(env, "java/lang/invoke/SerializedLambda");
javaLangInvokeSerializedLambdaCapturingClass = getFieldId(env, serializedLambda, "capturingClass", "Ljava/lang/Class;", false);

JNIObjectHandle sunUtilResourcesBundlesCacheKey = findClass(env, "sun/util/resources/Bundles$CacheKey");
sunUtilResourcesBundlesCacheKeyGetName = getMethodId(env, sunUtilResourcesBundlesCacheKey, "getName", "()Ljava/lang/String;", false);
sunUtilResourcesBundlesCacheKeyGetLocale = getMethodId(env, sunUtilResourcesBundlesCacheKey, "getLocale", "()Ljava/util/Locale;", false);

JNIObjectHandle javaLangModule = findClass(env, "java/lang/Module");
javaLangModuleGetName = getMethodId(env, javaLangModule, "getName", "()Ljava/lang/String;", false);

javaUtilLocale = newClassGlobalRef(env, "java/util/Locale");
javaUtilLocaleGetLanguage = getMethodId(env, javaUtilLocale, "getLanguage", "()Ljava/lang/String;", false);
javaUtilLocaleGetCountry = getMethodId(env, javaUtilLocale, "getCountry", "()Ljava/lang/String;", false);
javaUtilLocaleGetVariant = getMethodId(env, javaUtilLocale, "getVariant", "()Ljava/lang/String;", false);
javaUtilLocaleForLanguageTag = getMethodId(env, javaUtilLocale, "forLanguageTag", "(Ljava/lang/String;)Ljava/util/Locale;", true);
javaUtilLocaleEquals = getMethodId(env, javaUtilLocale, "equals", "(Ljava/lang/Object;)Z", false);
javaUtilLocaleToLanguageTag = getMethodId(env, javaUtilLocale, "toLanguageTag", "()Ljava/lang/String;", false);
}

JNIMethodId getJavaLangReflectExecutableGetParameterTypes(JNIEnvironment env) {
Expand Down Expand Up @@ -244,30 +249,6 @@ JNIMethodId getJavaLangReflectProxyIsProxyClass(JNIEnvironment env) {
return javaLangReflectProxyIsProxyClass;
}

public JNIMethodId getJavaUtilLocaleToLanguageTag(JNIEnvironment env) {
if (javaUtilLocaleToLanguageTag.isNull()) {
JNIObjectHandle javaUtilLocale = findClass(env, "java/util/Locale");
javaUtilLocaleToLanguageTag = getMethodId(env, javaUtilLocale, "toLanguageTag", "()Ljava/lang/String;", false);
}
return javaUtilLocaleToLanguageTag;
}

public JNIFieldId getJavaUtilResourceBundleParentField(JNIEnvironment env) {
if (javaUtilResourceBundleParentField.isNull()) {
JNIObjectHandle javaUtilResourceBundle = findClass(env, "java/util/ResourceBundle");
javaUtilResourceBundleParentField = getFieldId(env, javaUtilResourceBundle, "parent", "Ljava/util/ResourceBundle;", false);
}
return javaUtilResourceBundleParentField;
}

public JNIMethodId getJavaUtilResourceBundleGetLocale(JNIEnvironment env) {
if (javaUtilResourceBundleGetLocale.isNull()) {
JNIObjectHandle javaUtilResourceBundle = findClass(env, "java/util/ResourceBundle");
javaUtilResourceBundleGetLocale = getMethodId(env, javaUtilResourceBundle, "getLocale", "()Ljava/util/Locale;", false);
}
return javaUtilResourceBundleGetLocale;
}

public JNIMethodId getJavaLangInvokeCallSiteMakeSite(JNIEnvironment env) {
if (javaLangInvokeCallSiteMakeSite.isNull()) {
JNIObjectHandle javaLangInvokeCallSite = findClass(env, "java/lang/invoke/CallSite");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public final class AccessAdvisor {
internalCallerFilter.addOrGetChildren("java.util.**", ConfigurationFilter.Inclusion.Exclude);
internalCallerFilter.addOrGetChildren("java.util.concurrent.atomic.*", ConfigurationFilter.Inclusion.Include); // Atomic*FieldUpdater
internalCallerFilter.addOrGetChildren("java.util.Collections", ConfigurationFilter.Inclusion.Include); // java.util.Collections.zeroLengthArray
// LogRecord.readObject looks up resource bundles
internalCallerFilter.addOrGetChildren("java.util.logging.LogRecord", ConfigurationFilter.Inclusion.Include);
internalCallerFilter.addOrGetChildren("java.util.random.*", ConfigurationFilter.Inclusion.Include); // RandomGeneratorFactory$$Lambda
/*
* ForkJoinTask.getThrowableException calls Class.getConstructors and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,6 @@ public void processEntry(EconomicMap<String, ?> entry, ConfigurationSet configur
break;
}

case "putBundleInCache":
case "getBundleImpl": {
expectSize(args, 5);
String baseName = (String) args.get(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,13 @@ public boolean isNotIncluded(String bundleName) {
}

@Override
public void prepareBundle(String bundleName, ResourceBundle bundle, Function<String, Optional<Module>> findModule, Locale locale) {
super.prepareBundle(bundleName, bundle, findModule, locale);
public void prepareBundle(String bundleName, ResourceBundle bundle, Function<String, Optional<Module>> findModule, Locale locale, boolean jdkLocale) {
super.prepareBundle(bundleName, bundle, findModule, locale, jdkLocale);
/* Initialize ResourceBundle.keySet eagerly */
bundle.keySet();
this.existingBundles.add(control.toBundleName(bundleName, locale));
if (!jdkLocale) {
this.existingBundles.add(control.toBundleName(bundleName, locale));
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
import static com.oracle.svm.util.StringUtil.toSlashSeparated;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IllformedLocaleException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
Expand All @@ -43,6 +45,7 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import org.graalvm.collections.EconomicSet;
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.Platform;
import org.graalvm.nativeimage.Platforms;
Expand All @@ -55,8 +58,11 @@
import com.oracle.svm.core.SubstrateUtil;
import com.oracle.svm.core.jdk.Resources;
import com.oracle.svm.core.util.VMError;
import com.oracle.svm.util.ReflectionUtil;

import jdk.graal.compiler.debug.GraalError;
import sun.util.locale.provider.LocaleProviderAdapter;
import sun.util.locale.provider.ResourceBundleBasedAdapter;
import sun.util.resources.Bundles;

/**
Expand All @@ -77,8 +83,12 @@ public class LocalizationSupport {

public final ResourceBundle.Control control = ResourceBundle.Control.getControl(ResourceBundle.Control.FORMAT_DEFAULT);

private final Bundles.Strategy strategy = getLocaleDataStrategy();

public final Charset defaultCharset;

private final EconomicSet<String> registeredBundles = EconomicSet.create();

public LocalizationSupport(Locale defaultLocale, Set<Locale> locales, Charset defaultCharset) {
this.defaultLocale = defaultLocale;
this.allLocales = locales.toArray(new Locale[0]);
Expand Down Expand Up @@ -107,13 +117,23 @@ public Map<String, Object> getBundleContentOf(Object bundle) {
throw VMError.unsupportedFeature("Resource bundle lookup must be loaded during native image generation: " + bundle.getClass());
}

private static Bundles.Strategy getLocaleDataStrategy() {
try {
Class<?> localeDataStrategy = ReflectionUtil.lookupClass(false, "sun.util.resources.LocaleData$LocaleDataStrategy");
Field strategyInstance = ReflectionUtil.lookupField(localeDataStrategy, "INSTANCE");
return (Bundles.Strategy) strategyInstance.get(null);
} catch (IllegalAccessException e) {
throw VMError.shouldNotReachHere(e);
}
}

@Platforms(Platform.HOSTED_ONLY.class)
public void prepareBundle(String bundleName, ResourceBundle bundle, Function<String, Optional<Module>> findModule, Locale locale) {
public void prepareBundle(String bundleName, ResourceBundle bundle, Function<String, Optional<Module>> findModule, Locale locale, boolean jdkBundle) {
/*
* Class-based bundle lookup happens on every query, but we don't need to register the
* constructor for a property resource bundle since the class lookup will fail.
*/
registerRequiredReflectionAndResourcesForBundle(bundleName, Set.of(locale));
registerRequiredReflectionAndResourcesForBundle(bundleName, Set.of(locale), jdkBundle);
if (!(bundle instanceof PropertyResourceBundle)) {
registerNullaryConstructor(bundle.getClass());
}
Expand Down Expand Up @@ -171,33 +191,59 @@ private String getBundleName(String fixedBundleName, Locale locale) {
}
}

public void registerRequiredReflectionAndResourcesForBundle(String baseName, Collection<Locale> wantedLocales) {
int i = baseName.lastIndexOf('.');
if (i > 0) {
String name = baseName.substring(i + 1) + "Provider";
String providerName = baseName.substring(0, i) + ".spi." + name;
ImageSingletons.lookup(RuntimeReflectionSupport.class).registerClassLookup(ConfigurationCondition.alwaysTrue(), providerName);
public void registerRequiredReflectionAndResourcesForBundle(String baseName, Collection<Locale> wantedLocales, boolean jdkBundle) {
if (!jdkBundle) {
int i = baseName.lastIndexOf('.');
if (i > 0) {
String name = baseName.substring(i + 1) + "Provider";
String providerName = baseName.substring(0, i) + ".spi." + name;
ImageSingletons.lookup(RuntimeReflectionSupport.class).registerClassLookup(ConfigurationCondition.alwaysTrue(), providerName);
}
}

ImageSingletons.lookup(RuntimeReflectionSupport.class).registerClassLookup(ConfigurationCondition.alwaysTrue(), baseName);

for (Locale locale : wantedLocales) {
registerRequiredReflectionAndResourcesForBundleAndLocale(baseName, locale);
registerRequiredReflectionAndResourcesForBundleAndLocale(baseName, locale, jdkBundle);
}
}

private void registerRequiredReflectionAndResourcesForBundleAndLocale(String baseName, Locale baseLocale) {
for (Locale locale : control.getCandidateLocales(baseName, baseLocale)) {
String bundleWithLocale = control.toBundleName(baseName, locale);
public void registerRequiredReflectionAndResourcesForBundleAndLocale(String baseName, Locale baseLocale, boolean jdkBundle) {
/*
* Bundles in the sun.(text|util).resources.cldr packages are loaded with an alternative
* strategy which tries parent aliases defined in CLDRBaseLocaleDataMetaInfo.parentLocales.
*/
List<Locale> candidateLocales = jdkBundle
? getJDKBundleCandidateLocales(baseName, baseLocale)
: control.getCandidateLocales(baseName, baseLocale);

for (Locale locale : candidateLocales) {
String bundleWithLocale = jdkBundle ? strategy.toBundleName(baseName, locale) : control.toBundleName(baseName, locale);
RuntimeReflection.registerClassLookup(bundleWithLocale);
Class<?> bundleClass = ReflectionUtil.lookupClass(true, bundleWithLocale);
if (bundleClass != null) {
registerNullaryConstructor(bundleClass);
}
Resources.singleton().registerNegativeQuery(bundleWithLocale.replace('.', '/') + ".properties");
String otherBundleName = Bundles.toOtherBundleName(baseName, bundleWithLocale, locale);
if (!otherBundleName.equals(bundleWithLocale)) {
RuntimeReflection.registerClassLookup(otherBundleName);

if (jdkBundle) {
String otherBundleName = Bundles.toOtherBundleName(baseName, bundleWithLocale, locale);
if (!otherBundleName.equals(bundleWithLocale)) {
RuntimeReflection.registerClassLookup(otherBundleName);
}
}
}
}

private static List<Locale> getJDKBundleCandidateLocales(String baseName, Locale baseLocale) {
/*
* LocaleDataStrategy.getCandidateLocale does some filtering of locales it knows do not have
* a bundle for the requested base name. We still want to see those locales to be able to
* register negative queries for them.
*/
LocaleProviderAdapter.Type adapterType = baseName.contains(".cldr") ? LocaleProviderAdapter.Type.CLDR : LocaleProviderAdapter.Type.JRE;
ResourceBundleBasedAdapter adapter = ((ResourceBundleBasedAdapter) LocaleProviderAdapter.forType(adapterType));
return adapter.getCandidateLocales(baseName, baseLocale);
}

/**
* Template method for subclasses to perform additional tasks.
*/
Expand Down Expand Up @@ -273,4 +319,17 @@ private static void registerNullaryConstructor(Class<?> bundleClass) {
}
RuntimeReflection.register(nullaryConstructor);
}

@Platforms(Platform.HOSTED_ONLY.class)
public void registerBundleLookup(String baseName) {
registeredBundles.add(baseName);
}

public boolean isRegisteredBundleLookup(String baseName, Locale locale, Object controlOrStrategy) {
if (baseName == null || locale == null || controlOrStrategy == null) {
/* Those cases will throw a NullPointerException before any lookup */
return true;
}
return registeredBundles.contains(baseName);
}
}
Loading

0 comments on commit 60e1fdb

Please sign in to comment.