Skip to content

Commit 59a54e1

Browse files
lukesandbergGuice Team
authored and
Guice Team
committed
Fix some scalabilitiy issues in method handles
* avoid `asCollector` when the size is not bound by an existing method - otherwise we can easily hit limits in the number of method arguments that java allows which is 255. This was a problem for some of our multibinders since multibinders can easily have hundres or thousands of entries * be careful about deep `foldArguments` chains, method handles use a tail-call pattern which can lead to stack overflow, so instead use simple divide and conquor techniques. * detect when we are binding to very high arity methods and bail out. `MethodHandle` infra reserves 1 or two parameters and thus limits the methods/constructors we can bind to to those with _only_ 253 or 254 parameters (WHAT IS THIS, A VM FOR ANTS!). - This should probably become a guice error, but for the time being we just back off. PiperOrigin-RevId: 741507607
1 parent fba0e10 commit 59a54e1

File tree

9 files changed

+1481
-52
lines changed

9 files changed

+1481
-52
lines changed

core/src/com/google/inject/internal/DefaultConstructionProxyFactory.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.inject.internal;
1818

19+
import static com.google.inject.internal.InternalMethodHandles.CONSTRUCTOR_NEWINSTANCE_HANDLE;
1920
import static com.google.inject.internal.InternalMethodHandles.castReturnTo;
2021
import static com.google.inject.internal.InternalMethodHandles.castReturnToObject;
2122
import static java.lang.invoke.MethodType.methodType;
@@ -134,6 +135,9 @@ public MethodHandle getConstructHandle(MethodHandle[] parameterHandles) {
134135
.bindTo(fastConstructor)
135136
.asType(methodType(Object.class, Object.class, Object[].class));
136137
handle = MethodHandles.insertArguments(handle, 0, (Object) null); // no receiver type.
138+
// NOTE: is is safe to use asCollector here because the number of parameters is the same
139+
// as the number of parameters to the constructor which should never exceed the maxiumum
140+
// number of method parameters.
137141
handle = handle.asCollector(Object[].class, parameterHandles.length);
138142
// Pass all the parameters to the constructor.
139143
handle = MethodHandles.filterArguments(handle, 0, parameterHandles);
@@ -163,8 +167,15 @@ public T newInstance(Object... arguments) throws InvocationTargetException {
163167

164168
@Override
165169
public MethodHandle getConstructHandle(MethodHandle[] parameterHandles) {
166-
// should be impossible to get here with methodhandles enabled
167-
throw new AssertionError("impossible");
170+
// See comments in ProviderMethod on how this rarely happens and why it happens
171+
var handle = CONSTRUCTOR_NEWINSTANCE_HANDLE.bindTo(constructor);
172+
// collect the parameters into an array of type Object[]
173+
handle = handle.asCollector(Object[].class, parameterHandles.length);
174+
// apply all the parameters to the constructor.
175+
handle = MethodHandles.filterArguments(handle, 0, parameterHandles);
176+
// merge all the internalcontext parameters into a single object factory.
177+
return MethodHandles.permuteArguments(
178+
handle, InternalMethodHandles.FACTORY_TYPE, new int[parameterHandles.length]);
168179
}
169180
}
170181

core/src/com/google/inject/internal/InternalMethodHandles.java

Lines changed: 152 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@
5454
import java.lang.reflect.Constructor;
5555
import java.lang.reflect.Field;
5656
import java.lang.reflect.InaccessibleObjectException;
57+
import java.lang.reflect.InvocationTargetException;
5758
import java.lang.reflect.Method;
59+
import java.lang.reflect.Modifier;
5860
import java.util.List;
5961
import java.util.Map;
6062
import java.util.concurrent.ConcurrentHashMap;
@@ -93,6 +95,41 @@ public final class InternalMethodHandles {
9395
findVirtualOrDie(
9496
BiFunction.class, "apply", methodType(Object.class, Object.class, Object.class));
9597

98+
/**
99+
* A handle for {@link Method#invoke}, useful when we can neither use a fastclass or direct method
100+
* handle.
101+
*/
102+
static final MethodHandle METHOD_INVOKE_HANDLE =
103+
findVirtualOrDie(
104+
Method.class, "invoke", methodType(Object.class, Object.class, Object[].class));
105+
106+
/**
107+
* A handle for {@link Method#invoke}, useful when we can neither use a fastclass or direct method
108+
* handle.
109+
*/
110+
static final MethodHandle CONSTRUCTOR_NEWINSTANCE_HANDLE =
111+
findVirtualOrDie(Constructor.class, "newInstance", methodType(Object.class, Object[].class));
112+
113+
/**
114+
* The maximum arity of a method handle that can be bound.
115+
*
116+
* <ul>
117+
* <li>There is a hard limit imposed by the JVM of 255 parameters.
118+
* <li>MethodHandles add another parameter to represent the 'direct bound handle'
119+
* <li>A MethodHandle that is invoked from Java by `invokeExact` adds another parameter to
120+
* represent the receiver `MethodHandle` of `invokeExact`.
121+
* </ul>
122+
*
123+
* <p>This leaves us with 255 - 2 = 253 parameters as the maximum arity of a method handle that
124+
* can be bound.
125+
*
126+
* <p>TODO(b/366058184): For now we use this to decide when to back off and have callers
127+
* workaround generally by generating a fastclass. We should just enforce this limit on users
128+
* basically no one should really need to use so many parameters in a constructor/method and for
129+
* guice injectable methods the workarounds are generally quite simple anyway, so we can just make
130+
* this an error.
131+
*/
132+
private static final int MAX_BINDABLE_ARITY = 253;
96133

97134
/**
98135
* Returns a method handle for the given method, or null if the method is not accessible.
@@ -102,6 +139,14 @@ public final class InternalMethodHandles {
102139
*/
103140
@Nullable
104141
static MethodHandle unreflect(Method method) {
142+
// account for the `this` param if any
143+
int paramSize = Modifier.isStatic(method.getModifiers()) ? 1 : 0;
144+
for (var param : method.getParameterTypes()) {
145+
paramSize += param.isPrimitive() ? Type.getType(param).getSize() : 1;
146+
}
147+
if (paramSize > MAX_BINDABLE_ARITY) {
148+
return null;
149+
}
105150
try {
106151
method.setAccessible(true);
107152
} catch (SecurityException | InaccessibleObjectException e) {
@@ -123,6 +168,13 @@ static MethodHandle unreflect(Method method) {
123168
*/
124169
@Nullable
125170
static MethodHandle unreflectConstructor(Constructor<?> ctor) {
171+
int paramSize = 1; // constructors always have a `this` param
172+
for (var param : ctor.getParameterTypes()) {
173+
paramSize += param.isPrimitive() ? Type.getType(param).getSize() : 1;
174+
}
175+
if (paramSize > MAX_BINDABLE_ARITY) {
176+
return null;
177+
}
126178
try {
127179
ctor.setAccessible(true);
128180
} catch (SecurityException | InaccessibleObjectException e) {
@@ -683,6 +735,12 @@ static MethodHandle tryStartConstruction(
683735
return MethodHandles.foldArguments(guard, tryStartConstruction);
684736
}
685737

738+
private static final MethodHandle TRY_START_CONSTRUCTION_HANDLE =
739+
findVirtualOrDie(
740+
InternalContext.class,
741+
"tryStartConstruction",
742+
methodType(Object.class, int.class, Dependency.class));
743+
686744
/**
687745
* Drops the return value from a method handle.
688746
*
@@ -705,11 +763,24 @@ private static boolean isNull(Object result) {
705763
return result == null;
706764
}
707765

708-
private static final MethodHandle TRY_START_CONSTRUCTION_HANDLE =
709-
findVirtualOrDie(
710-
InternalContext.class,
711-
"tryStartConstruction",
712-
methodType(Object.class, int.class, Dependency.class));
766+
static MethodHandle catchInvocationTargetExceptionAndRethrowCause(MethodHandle handle) {
767+
return MethodHandles.catchException(
768+
handle,
769+
InvocationTargetException.class,
770+
CATCH_INVOCATION_TARGET_EXCEPTION_AND_RETHROW_CAUSE_HANDLE);
771+
}
772+
773+
private static final MethodHandle CATCH_INVOCATION_TARGET_EXCEPTION_AND_RETHROW_CAUSE_HANDLE =
774+
findStaticOrDie(
775+
InternalMethodHandles.class,
776+
"doCatchInvocationTargetExceptionAndRethrowCause",
777+
methodType(Object.class, InvocationTargetException.class));
778+
779+
@Keep
780+
private static Object doCatchInvocationTargetExceptionAndRethrowCause(InvocationTargetException e)
781+
throws Throwable {
782+
throw e.getCause();
783+
}
713784

714785
/**
715786
* Generates a provider instance that delegates to the given factory.
@@ -1264,18 +1335,45 @@ static MethodHandle buildImmutableSetFactory(Iterable<MethodHandle> elementHandl
12641335
var builder =
12651336
MethodHandles.insertArguments(
12661337
IMMUTABLE_SET_BUILDER_OF_SIZE_HANDLE, 0, elementHandlesList.size());
1267-
// Add any InternalContext arguments to the builder.
1268-
builder = MethodHandles.dropArguments(builder, 0, InternalContext.class);
1269-
for (var handle : elementHandlesList) {
1270-
// builder = builder.add(<handle>(ctx))
1271-
builder =
1272-
MethodHandles.foldArguments(
1273-
MethodHandles.filterArguments(IMMUTABLE_SET_BUILDER_ADD_HANDLE, 1, handle), builder);
1274-
}
1338+
1339+
builder = MethodHandles.foldArguments(doAddToImmutableSet(elementHandlesList), builder);
12751340
return MethodHandles.filterReturnValue(builder, IMMUTABLE_SET_BUILDER_BUILD_HANDLE)
12761341
.asType(FACTORY_TYPE);
12771342
}
12781343

1344+
/**
1345+
* A helper to generate calls to add to the ImmutableSet builder.
1346+
*
1347+
* <p>Returns a handle of type (ImmutableSet.Builder, InternalContext) -> ImmutableSet.Builder
1348+
*/
1349+
private static MethodHandle doAddToImmutableSet(ImmutableList<MethodHandle> elementHandlesList) {
1350+
// We don't want to 'fold' too deep as it can lead to a stack overflow. So we have a special
1351+
// case for small lists.
1352+
if (elementHandlesList.size() < 32) {
1353+
var builder =
1354+
MethodHandles.dropArguments(
1355+
MethodHandles.identity(ImmutableSet.Builder.class), 1, InternalContext.class);
1356+
for (var handle : elementHandlesList) {
1357+
// builder = builder.add(<handle>(ctx))
1358+
builder =
1359+
MethodHandles.foldArguments(
1360+
MethodHandles.filterArguments(IMMUTABLE_SET_BUILDER_ADD_HANDLE, 1, handle),
1361+
dropReturn(builder));
1362+
}
1363+
return builder;
1364+
} else {
1365+
// Otherwise we split and recurse, this basically ensures that none of the lambda forms are
1366+
// too big and the 'folds' are not too deep.
1367+
int half = elementHandlesList.size() / 2;
1368+
var left = elementHandlesList.subList(0, half);
1369+
var right = elementHandlesList.subList(half, elementHandlesList.size());
1370+
// We are basically creating 2 methods in a chain that add to the builder
1371+
// We do this by calling `doAddToImmutableSet` recursively.
1372+
return MethodHandles.foldArguments(
1373+
doAddToImmutableSet(right), dropReturn(doAddToImmutableSet(left)));
1374+
}
1375+
}
1376+
12791377
private static final MethodHandle IMMUTABLE_SET_OF_HANDLE =
12801378
InternalMethodHandles.findStaticOrDie(
12811379
ImmutableSet.class, "of", methodType(ImmutableSet.class, Object.class));
@@ -1327,31 +1425,51 @@ static <T> MethodHandle buildImmutableMapFactory(List<Map.Entry<T, MethodHandle>
13271425
var builder =
13281426
MethodHandles.insertArguments(
13291427
IMMUTABLE_MAP_BUILDER_WITH_EXPECTED_SIZE_HANDLE, 0, entries.size());
1330-
builder = MethodHandles.dropArguments(builder, 0, InternalContext.class);
1331-
for (Map.Entry<T, MethodHandle> entry : entries) {
1332-
// `put` has the signature `put(Builder, K, V)->Builder` (the first parameter is 'this').
1333-
// Insert the 'constant' key to get this signature:
1334-
// (Builder, V)->Builder
1335-
var put = MethodHandles.insertArguments(IMMUTABLE_MAP_BUILDER_PUT_HANDLE, 1, entry.getKey());
1336-
// Construct the value, by calling the factory method handle to supply the first argument (the
1337-
// value). Because the entry is a MethodHandle with signature `(InternalContext)->V` we need
1338-
// to cast the return type to `Object` to match the signature of `put`.
1339-
// (Builder, InternalContext)->Builder
1340-
put = MethodHandles.filterArguments(put, 1, entry.getValue());
1341-
// Fold works by invoking the 'builder' and then passing it to the first argument of the
1342-
// `put` method, and then passing the arguments to `builder` to put as well. Like this:
1343-
// Builder fold(InternalContext ctx) {
1344-
// bar builder = <builder-handle>(ctx);
1345-
// return <put-handle>(builder, ctx);
1346-
// }
1347-
// (InternalContext)->Builder
1348-
// Now, that has the same signture as builder, so assign back and loop.
1349-
builder = MethodHandles.foldArguments(put, builder);
1350-
}
1428+
builder = MethodHandles.foldArguments(doPutEntries(entries), builder);
13511429
return MethodHandles.filterReturnValue(builder, IMMUTABLE_MAP_BUILDER_BUILD_OR_THROW_HANDLE)
13521430
.asType(FACTORY_TYPE);
13531431
}
13541432

1433+
/** Returns a handle of type (ImmutableMap.Builder, InternalContext) -> ImmutableMap.Builder */
1434+
private static <T> MethodHandle doPutEntries(List<Map.Entry<T, MethodHandle>> entries) {
1435+
int size = entries.size();
1436+
checkArgument(size > 0, "entries must not be empty");
1437+
if (size < 32) {
1438+
MethodHandle builder = null;
1439+
for (Map.Entry<T, MethodHandle> entry : entries) {
1440+
// `put` has the signature `put(Builder, K, V)->Builder` (the first parameter is 'this').
1441+
// Insert the 'constant' key to get this signature:
1442+
// (Builder, V)->Builder
1443+
var put =
1444+
MethodHandles.insertArguments(IMMUTABLE_MAP_BUILDER_PUT_HANDLE, 1, entry.getKey());
1445+
// Construct the value, by calling the factory method handle to supply the first argument
1446+
// (the
1447+
// value). Because the entry is a MethodHandle with signature `(InternalContext)->V` we
1448+
// need
1449+
// to cast the return type to `Object` to match the signature of `put`.
1450+
// (Builder, InternalContext)->Builder
1451+
put = MethodHandles.filterArguments(put, 1, entry.getValue());
1452+
// Fold works by invoking the 'builder' and then passing it to the first argument of the
1453+
// `put` method, and then passing the arguments to `builder` to put as well. Like this:
1454+
// Builder fold(InternalContext ctx) {
1455+
// bar builder = <builder-handle>(ctx);
1456+
// return <put-handle>(builder, ctx);
1457+
// }
1458+
// (InternalContext)->Builder
1459+
// Now, that has the same signture as builder, so assign back and loop.
1460+
builder = builder == null ? put : MethodHandles.foldArguments(put, dropReturn(builder));
1461+
}
1462+
return builder;
1463+
} else {
1464+
// Otherwise we split and recurse, this basically ensures that none of the lambda forms are
1465+
// too big and the 'folds' are not too deep.
1466+
int half = size / 2;
1467+
return MethodHandles.foldArguments(
1468+
doPutEntries(entries.subList(half, size)),
1469+
dropReturn(doPutEntries(entries.subList(0, half))));
1470+
}
1471+
}
1472+
13551473
private static final MethodHandle IMMUTABLE_MAP_OF_HANDLE =
13561474
InternalMethodHandles.findStaticOrDie(
13571475
ImmutableMap.class, "of", methodType(ImmutableMap.class, Object.class, Object.class));

core/src/com/google/inject/internal/ProviderMethod.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.inject.internal;
1818

1919
import static com.google.inject.internal.InternalMethodHandles.BIFUNCTION_APPLY_HANDLE;
20+
import static com.google.inject.internal.InternalMethodHandles.METHOD_INVOKE_HANDLE;
2021
import static com.google.inject.internal.InternalMethodHandles.castReturnTo;
2122
import static com.google.inject.internal.InternalMethodHandles.castReturnToObject;
2223
import static java.lang.invoke.MethodType.methodType;
@@ -349,7 +350,10 @@ MethodHandle doProvisionHandle(MethodHandle[] parameters) {
349350
MethodHandles.insertArguments(BIFUNCTION_APPLY_HANDLE.bindTo(fastMethod), 0, instance)
350351
// Cast the parameter type to be an Object array
351352
.asType(methodType(Object.class, Object[].class))
352-
// Allow it to be called as a varargs method.
353+
// Have it collect N arrguments into an array of type Object[]
354+
// This is safe because the number of parameters is the same as the number of
355+
// parameters to the method which should never exceed the maximum number of
356+
// method parameters.
353357
.asCollector(Object[].class, parameters.length);
354358

355359
// The signature is now (...InternaleContext)->Object, but we want to pass the same context
@@ -385,7 +389,31 @@ T doProvision(Object[] parameters) throws IllegalAccessException, InvocationTarg
385389

386390
@Override
387391
MethodHandle doProvisionHandle(MethodHandle[] parameters) {
388-
throw new AssertionError("impossible");
392+
// We can hit this case if
393+
// 1. the method/class/parameters are non-public and we are using bytecode gen but a security
394+
// manager or modules configuration are installed that blocks access to our fastclass
395+
// generation and the setAccessible method.
396+
// 2. The method has too many parameters to be supported by method handles (and fastclass also
397+
// fails)
398+
// So we fall back to reflection.
399+
// You might be wondering... why is it that MethodHandles cannot handle methods with the
400+
// maximum number of parameters but java reflection can? And yes it is true that java
401+
// reflection is based on MethodHandles, but it supports a fallback for exactly this case that
402+
// goes through a JVM native method... le sigh...
403+
404+
// bind to the `Method` object
405+
var handle = METHOD_INVOKE_HANDLE.bindTo(method);
406+
// insert the instance
407+
handle = MethodHandles.insertArguments(handle, 0, instance);
408+
// collect the parameters into an array of type Object[]
409+
handle = handle.asCollector(Object[].class, parameters.length);
410+
// supply all parameters
411+
handle = MethodHandles.filterArguments(handle, 0, parameters);
412+
// Merge all the parameters into a single object factory method.
413+
handle =
414+
MethodHandles.permuteArguments(
415+
handle, InternalMethodHandles.FACTORY_TYPE, new int[parameters.length]);
416+
return handle;
389417
}
390418
}
391419

@@ -430,6 +458,9 @@ MethodHandle doProvisionHandle(MethodHandle[] parameters) {
430458
handle =
431459
MethodHandles.permuteArguments(
432460
handle, InternalMethodHandles.FACTORY_TYPE, new int[parameters.length]);
461+
// MethodHandles expect the root Throwable to be thrown directly, unpack and rethrow the
462+
// cause instead of complicating the normal flow.
463+
handle = InternalMethodHandles.catchInvocationTargetExceptionAndRethrowCause(handle);
433464
return handle;
434465
}
435466

core/src/com/google/inject/internal/ProxyFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ public MethodHandle getConstructHandle(MethodHandle[] parameterHandles) {
195195
.asType(methodType(Object.class, Object.class, Object[].class));
196196
handle = MethodHandles.insertArguments(handle, 0, (Object) callbacks);
197197
// Turn the array parameter into a fixed set of parameters.
198+
// This is safe because the number of parameters is the same as the number of parameters to
199+
// the constructor which should never exceed the maximum number of method parameters.
198200
handle = handle.asCollector(Object[].class, parameterHandles.length);
199201
// Pass all the parameters to the constructor.
200202
handle = MethodHandles.filterArguments(handle, 0, parameterHandles);

0 commit comments

Comments
 (0)