Skip to content

Commit ee772e0

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 c2b7cdd commit ee772e0

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;
@@ -335,7 +336,7 @@ public final Object invoke(final Object object, final Object[] arguments) {
335336
}
336337

337338
try {
338-
return cachedMethod.invoke(object, arguments);
339+
return normalizeBoxedReturn(cachedMethod.invoke(object, arguments));
339340
} catch (IllegalArgumentException | IllegalAccessException e) {
340341
throw new InvokerInvocationException(e);
341342
} catch (InvocationTargetException e) {
@@ -344,6 +345,16 @@ public final Object invoke(final Object object, final Object[] arguments) {
344345
}
345346
}
346347

348+
/**
349+
* Keeps reference identity ({@code ===}) of primitive returns consistent
350+
* across dispatch paths (GROOVY-12140).
351+
*
352+
* @see MetaClassHelper#normalizeBoxedReturn(Object, Class)
353+
*/
354+
private Object normalizeBoxedReturn(final Object value) {
355+
return MetaClassHelper.normalizeBoxedReturn(value, cachedMethod.getReturnType());
356+
}
357+
347358
private void makeAccessibleIfNecessary() {
348359
if (!makeAccessibleDone) {
349360
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
@@ -1015,6 +1015,30 @@ public static void unwrap(Object[] arguments) {
10151015
}
10161016
}
10171017

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