Skip to content

Commit 23780cf

Browse files
committed
GROOVY-12140: normalize reflectively boxed primitive returns through valueOf
Method#invoke boxes primitive return values with fresh instances, unlike MethodHandle-based and generated-bytecode invocation, which box through the valueOf caches. Dispatch paths that invoke reflectively therefore broke reference identity (===) of primitive returns: classic-mode reflective call sites (e.g. varargs methods), and — in both compilation modes — anything routed through MetaMethod#doMethodInvoke, notably dynamic-name calls such as obj."$name"(), where a constant-name call to the same method returns the cached box. Reflective results are now re-normalized through the valueOf caches at the two chokepoints: CachedMethod#invoke (all doMethodInvoke routes) and PlainObjectMetaMethodSite#doInvoke (classic call sites holding a raw Method). The shared helper lives in MetaClassHelper#normalizeBoxedReturn. float/double are left as-is since valueOf does not cache them on any path. Also reproducible on GROOVY_5_0_X, so a candidate for backport.
1 parent 58162cf commit 23780cf

4 files changed

Lines changed: 105 additions & 2 deletions

File tree

src/main/java/org/codehaus/groovy/reflection/CachedMethod.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import groovy.lang.MissingMethodException;
2424
import org.codehaus.groovy.classgen.asm.BytecodeHelper;
2525
import org.codehaus.groovy.runtime.InvokerInvocationException;
26+
import org.codehaus.groovy.runtime.MetaClassHelper;
2627
import org.codehaus.groovy.runtime.callsite.CallSite;
2728
import org.codehaus.groovy.runtime.callsite.CallSiteGenerator;
2829
import org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite;
@@ -499,7 +500,7 @@ public final Object invoke(final Object object, final Object[] arguments) {
499500
makeAccessibleIfNecessary();
500501

501502
try {
502-
return cachedMethod.invoke(object, arguments);
503+
return normalizeBoxedReturn(cachedMethod.invoke(object, arguments));
503504
} catch (IllegalArgumentException | IllegalAccessException e) {
504505
throw new InvokerInvocationException(e);
505506
} catch (InvocationTargetException e) {
@@ -508,6 +509,16 @@ public final Object invoke(final Object object, final Object[] arguments) {
508509
}
509510
}
510511

512+
/**
513+
* Keeps reference identity ({@code ===}) of primitive returns consistent
514+
* across dispatch paths (GROOVY-12140).
515+
*
516+
* @see MetaClassHelper#normalizeBoxedReturn(Object, Class)
517+
*/
518+
private Object normalizeBoxedReturn(final Object value) {
519+
return MetaClassHelper.normalizeBoxedReturn(value, cachedMethod.getReturnType());
520+
}
521+
511522
private void makeAccessibleIfNecessary() {
512523
if (!makeAccessibleDone) {
513524
ReflectionUtils.makeAccessibleInPrivilegedAction(cachedMethod);

src/main/java/org/codehaus/groovy/runtime/MetaClassHelper.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,30 @@ public static void unwrap(Object[] arguments) {
10091009
}
10101010
}
10111011

1012+
/**
1013+
* Re-normalizes a reflectively boxed primitive return value through the
1014+
* {@code valueOf} caches. {@link java.lang.reflect.Method#invoke} boxes
1015+
* primitive returns with fresh instances, unlike MethodHandle-based and
1016+
* generated-bytecode invocation, which box through the caches; without
1017+
* normalization, reference identity ({@code ===}) of primitive returns
1018+
* would differ between dispatch paths (GROOVY-12140).
1019+
*
1020+
* @param value the reflectively boxed return value (may be {@code null})
1021+
* @param returnType the declared return type of the invoked method
1022+
* @return the cache-normalized value
1023+
* @since 6.0.0
1024+
*/
1025+
public static Object normalizeBoxedReturn(final Object value, final Class<?> returnType) {
1026+
if (value == null || !returnType.isPrimitive()) return value; // null includes void
1027+
if (returnType == int.class) return Integer.valueOf((Integer) value);
1028+
if (returnType == boolean.class) return Boolean.valueOf((Boolean) value);
1029+
if (returnType == long.class) return Long.valueOf((Long) value);
1030+
if (returnType == char.class) return Character.valueOf((Character) value);
1031+
if (returnType == byte.class) return Byte.valueOf((Byte) value);
1032+
if (returnType == short.class) return Short.valueOf((Short) value);
1033+
return value; // float/double: valueOf does not cache on any path
1034+
}
1035+
10121036
/**
10131037
* Sets the metaclass for an object, by delegating to the appropriate
10141038
* {@link DefaultGroovyMethods} helper method. This method was introduced as

src/main/java/org/codehaus/groovy/runtime/callsite/PlainObjectMetaMethodSite.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import groovy.lang.GroovyRuntimeException;
2323
import groovy.lang.MetaClass;
2424
import groovy.lang.MetaMethod;
25+
import org.codehaus.groovy.runtime.MetaClassHelper;
2526
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
2627

2728
import java.lang.reflect.InvocationTargetException;
@@ -40,7 +41,8 @@ public PlainObjectMetaMethodSite(CallSite site, MetaClass metaClass, MetaMethod
4041

4142
protected static Object doInvoke(Object receiver, Object[] args, Method reflect) throws Throwable {
4243
try {
43-
return reflect.invoke(receiver, args);
44+
// GROOVY-12140: keep === identity of primitive returns consistent
45+
return MetaClassHelper.normalizeBoxedReturn(reflect.invoke(receiver, args), reflect.getReturnType());
4446
} catch (InvocationTargetException e) {
4547
Throwable cause = e.getCause();
4648
if (cause instanceof GroovyRuntimeException) {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package bugs
20+
21+
import org.codehaus.groovy.control.CompilerConfiguration
22+
import org.junit.jupiter.api.Test
23+
24+
import static groovy.test.GroovyAssert.assertScript
25+
26+
final class Groovy12140 {
27+
28+
private static final String SCRIPT = '''
29+
class W {
30+
int num(int... xs) { 42 }
31+
int val() { 42 }
32+
char ch() { (char) 'a' }
33+
boolean flag() { true }
34+
long lng() { 42L }
35+
byte byt() { (byte) 42 }
36+
short shrt() { (short) 42 }
37+
}
38+
def w = new W()
39+
def name = 'val'
40+
// dynamic-name calls dispatch reflectively via doMethodInvoke, in both
41+
// compilation modes: primitive returns must box through the valueOf
42+
// caches for identity (===) parity with directly dispatched calls
43+
// (float/double are deliberately not asserted: valueOf caches nothing
44+
// for them on any dispatch path)
45+
assert w."$name"() === 42
46+
assert w."${'ch'}"() === Character.valueOf((char) 'a')
47+
assert w."${'flag'}"() === Boolean.TRUE
48+
assert w."${'lng'}"() === Long.valueOf(42L)
49+
assert w."${'byt'}"() === Byte.valueOf((byte) 42)
50+
assert w."${'shrt'}"() === Short.valueOf((short) 42)
51+
// varargs methods use the reflective call-site variant under classic
52+
assert w.num(1, 2) === 42
53+
'''
54+
55+
@Test
56+
void testPrimitiveReturnBoxIdentity() {
57+
assertScript SCRIPT
58+
}
59+
60+
@Test
61+
void testPrimitiveReturnBoxIdentityClassic() {
62+
def config = new CompilerConfiguration()
63+
config.optimizationOptions.indy = false
64+
new GroovyShell(config).evaluate SCRIPT
65+
}
66+
}

0 commit comments

Comments
 (0)