Skip to content

Commit a710477

Browse files
lukesandbergGuice Team
authored andcommitted
Add methodhandle support to InternalFactoryToScopedProviderAdapter
We can take advantage of the fact that `setDependency` is a no-op when `disableCircularProxies` is set, but otherwise this is a pretty straightforward translation. For Singleton scoped factories, we can follow the pattern of `Initializable` and create a 'patchable' callsite which should enable constant folding of resolved singletons. PiperOrigin-RevId: 739912256
1 parent 6c33cd3 commit a710477

File tree

6 files changed

+183
-14
lines changed

6 files changed

+183
-14
lines changed

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

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

1919
import static com.google.common.base.Preconditions.checkNotNull;
20+
import static com.google.inject.internal.InternalMethodHandles.castReturnTo;
21+
import static com.google.inject.internal.InternalMethodHandles.castReturnToObject;
22+
import static com.google.inject.internal.InternalMethodHandles.findVirtualOrDie;
23+
import static java.lang.invoke.MethodType.methodType;
2024

25+
import com.google.errorprone.annotations.Keep;
2126
import com.google.errorprone.annotations.concurrent.LazyInit;
2227
import com.google.inject.Provider;
2328
import com.google.inject.Scope;
2429
import com.google.inject.spi.Dependency;
30+
import java.lang.invoke.MethodHandle;
31+
import java.lang.invoke.MethodHandles;
32+
import java.lang.invoke.MutableCallSite;
2533

2634
/**
2735
* A factory that wraps a provider that has been scoped.
@@ -66,6 +74,37 @@ public T get(InternalContext context, Dependency<?> dependency, boolean linked)
6674
}
6775
}
6876

77+
@Override
78+
public MethodHandle getHandle(LinkageContext context, Dependency<?> dependency, boolean linked) {
79+
// Call Provider.get() on the constant provider
80+
// ()->Object
81+
var invokeProvider =
82+
InternalMethodHandles.getProvider(
83+
MethodHandles.constant(jakarta.inject.Provider.class, provider));
84+
85+
// null check the result using the dependency.
86+
// ()->Object
87+
invokeProvider = InternalMethodHandles.nullCheckResult(invokeProvider, source, dependency);
88+
// Catch any RuntimeException as an InternalProvisionException.
89+
// ()->Object
90+
invokeProvider =
91+
InternalMethodHandles.catchErrorInProviderAndRethrowWithSource(invokeProvider, source);
92+
// (InternalContext) -> Object
93+
// Add an InternalContext argument to match the factory protocol.
94+
invokeProvider = MethodHandles.dropArguments(invokeProvider, 0, InternalContext.class);
95+
// We need to call 'setDependency' so it is available to scope implementations and scope
96+
// delegate providers. See comment in `get` method for more details.
97+
invokeProvider =
98+
MethodHandles.foldArguments(
99+
invokeProvider,
100+
MethodHandles.insertArguments(INTERNAL_CONTEXT_SET_DEPENDENCY_HANDLE, 1, dependency));
101+
return castReturnTo(invokeProvider, dependency.getKey().getTypeLiteral().getRawType());
102+
}
103+
104+
private static final MethodHandle INTERNAL_CONTEXT_SET_DEPENDENCY_HANDLE =
105+
InternalMethodHandles.findVirtualOrDie(
106+
InternalContext.class, "setDependency", methodType(void.class, Dependency.class));
107+
69108
@Override
70109
public String toString() {
71110
return provider.toString();
@@ -91,15 +130,101 @@ static final class ForSingletonScope<T> extends InternalFactoryToScopedProviderA
91130
super(provider, source);
92131
}
93132

133+
@Override
134+
public T get(InternalContext context, Dependency<?> dependency, boolean linked)
135+
throws InternalProvisionException {
136+
Object value = this.value;
137+
if (value != UNINITIALIZED_VALUE) {
138+
// safe because we only store values of T or UNINITIALIZED_VALUE
139+
@SuppressWarnings("unchecked")
140+
T typedValue = (T) value;
141+
if (typedValue == null && !dependency.isNullable()) {
142+
InternalProvisionException.onNullInjectedIntoNonNullableDependency(source, dependency);
143+
}
144+
return typedValue;
145+
}
146+
T t = super.get(context, dependency, linked);
147+
if (!context.areCircularProxiesEnabled() || !BytecodeGen.isCircularProxy(t)) {
148+
// Avoid caching circular proxies.
149+
this.value = t;
150+
}
151+
return t;
152+
}
153+
154+
@Override
155+
public MethodHandle getHandle(
156+
LinkageContext context, Dependency<?> dependency, boolean linked) {
157+
// If it is somehow already initialized, we can return a constant handle.
158+
Object value = this.value;
159+
if (value != UNINITIALIZED_VALUE) {
160+
return getHandleForConstant(dependency, source, value);
161+
}
162+
// Otherwise we bind to a callsite that will patch itself once it is initialized.
163+
return new SingletonCallSite(super.getHandle(context, dependency, linked), dependency, source)
164+
.dynamicInvoker();
165+
}
166+
167+
private static MethodHandle getHandleForConstant(
168+
Dependency<?> dependency, Object source, Object value) {
169+
var handle = InternalMethodHandles.constantFactoryGetHandle(dependency, value);
170+
handle = InternalMethodHandles.nullCheckResult(handle, source, dependency);
171+
return handle;
172+
}
173+
174+
/**
175+
* A special callsite that allows us to take advantage of the singleton scope semantics.
176+
*
177+
* <p>After initialization we can patch the callsite with a constant, which should unlock some
178+
* inlining opportunities.
179+
*/
180+
static final class SingletonCallSite extends MutableCallSite {
181+
static final MethodHandle BOOTSTRAP_CALL_MH =
182+
findVirtualOrDie(
183+
SingletonCallSite.class,
184+
"boostrapCallSite",
185+
methodType(Object.class, InternalContext.class, Object.class));
186+
187+
private final Dependency<?> dependency;
188+
private final Object source;
189+
190+
SingletonCallSite(MethodHandle actualGetHandle, Dependency<?> dependency, Object source) {
191+
super(actualGetHandle.type());
192+
this.dependency = dependency;
193+
this.source = source;
194+
// Invoke the 'actual' handle and then pass the result to the `boostrapCallSite` method.
195+
// This will allow us to eventually 'fold' the result into the callsite.
196+
// (InternalContext, InternalContext) -> Object
197+
var invokeBootstrap =
198+
MethodHandles.filterArguments(
199+
BOOTSTRAP_CALL_MH.bindTo(this), 1, castReturnToObject(actualGetHandle));
200+
201+
// (InternalContext, InternalContext) -> T
202+
invokeBootstrap = castReturnTo(invokeBootstrap, actualGetHandle.type().returnType());
203+
// (InternalContext) -> T
204+
invokeBootstrap =
205+
MethodHandles.permuteArguments(invokeBootstrap, actualGetHandle.type(), new int[2]);
206+
setTarget(invokeBootstrap);
207+
}
208+
209+
@Keep
210+
Object boostrapCallSite(InternalContext context, Object result) {
211+
// Don't cache circular proxies.
212+
if (!context.areCircularProxiesEnabled() || !BytecodeGen.isCircularProxy(result)) {
213+
setTarget(getHandleForConstant(dependency, source, result));
214+
// This ensures that other threads will see the new target. This isn't strictly necessary
215+
// since the underlying provider is both ThreadSafe and idempotent, but it should improve
216+
// performance by giving the JIT and easy optimization opportunity.
217+
MutableCallSite.syncAll(new MutableCallSite[] {this});
218+
}
219+
// otherwise we shouldn't cache the result.
220+
return result;
221+
}
222+
}
223+
94224
private T getAndCache(InjectorImpl injector, Dependency<?> dependency)
95225
throws InternalProvisionException {
96226
try (InternalContext context = injector.enterContext()) {
97-
T t = get(context, dependency, /* linked= */ false);
98-
if (!context.areCircularProxiesEnabled() || !BytecodeGen.isCircularProxy(t)) {
99-
// Avoid caching circular proxies.
100-
this.value = t;
101-
}
102-
return t;
227+
return get(context, dependency, /* linked= */ false);
103228
}
104229
}
105230

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ static MethodHandle castReturnTo(MethodHandle handle, Class<?> returnType) {
105105
return handle.asType(handle.type().changeReturnType(returnType));
106106
}
107107

108+
private static final MethodHandle PROVIDER_GET_HANDLE =
109+
findVirtualOrDie(jakarta.inject.Provider.class, "get", methodType(Object.class));
110+
111+
/**
112+
* Returns a method handle that calls {@code <providerHandle>.get()}.
113+
*
114+
* <p>The returned handle has the same parameters as the delegate and returns {@link Object}.
115+
*/
116+
static MethodHandle getProvider(MethodHandle providerHandle) {
117+
return MethodHandles.filterReturnValue(
118+
// need to cast to jakarta.inject.Provider to exactly match the signature of Provider.get
119+
castReturnTo(providerHandle, jakarta.inject.Provider.class), PROVIDER_GET_HANDLE);
120+
}
121+
108122
/** Direct handle for {@link InternalFactory#get} */
109123
static final MethodHandle INTERNAL_FACTORY_GET_HANDLE =
110124
findVirtualOrDie(

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

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

1919
import static com.google.common.base.Preconditions.checkNotNull;
20-
import static com.google.inject.internal.InternalMethodHandles.findVirtualOrDie;
2120
import static java.lang.invoke.MethodType.methodType;
2221

2322
import com.google.inject.Key;
@@ -143,9 +142,6 @@ protected MethodHandle circularGetHandle(
143142
return createProviderAndPass.asType(InternalMethodHandles.makeFactoryType(dependency));
144143
}
145144

146-
private static final MethodHandle PROVIDER_GET_HANDLE =
147-
findVirtualOrDie(jakarta.inject.Provider.class, "get", methodType(Object.class));
148-
149145
/**
150146
* Returns a method handle that calls {@code providerHandle.get()} and catches any
151147
* RuntimeException as an InternalProvisionException.
@@ -158,7 +154,7 @@ protected MethodHandle provisionHandle(MethodHandle providerHandle, Dependency<?
158154
// Call Provider.get() and catch any RuntimeException as an InternalProvisionException.
159155
var invokeProvider =
160156
InternalMethodHandles.catchErrorInProviderAndRethrowWithSource(
161-
MethodHandles.filterReturnValue(providerHandle, PROVIDER_GET_HANDLE), source);
157+
InternalMethodHandles.getProvider(providerHandle), source);
162158
// Wrap in a try..finally.. that calls `finishConstruction` on the context.
163159
invokeProvider = InternalMethodHandles.finishConstruction(invokeProvider, circularFactoryId);
164160

core/test/com/google/inject/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ guice_test_suites(
138138
name = "gen_tests_stack_trace_%s_use_experimental_method_handles_%s" % (include_stack_trace_option, use_experimental_method_handles_option),
139139
args = [
140140
"--guice_include_stack_traces=%s" % include_stack_trace_option,
141+
"--guice_use_experimental_method_handles=%s" % use_experimental_method_handles_option,
141142
],
142143
jvm_flags = [
143144
# those 2 options are required for some tests that checks stack traces

core/test/com/google/inject/errors/BUILD

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,20 @@ filegroup(
4646
)
4747

4848
[guice_test_suites(
49-
name = "gen_tests_stack_trace%s" % include_stack_trace_option,
49+
name = "gen_tests_stack_trace%s_use_experimental_method_handles_%s" % (include_stack_trace_option, use_experimental_method_handles_option),
5050
args = [
5151
"--guice_include_stack_traces=%s" % include_stack_trace_option,
52+
"--guice_use_experimental_method_handles=%s" % use_experimental_method_handles_option,
5253
],
5354
sizes = [
5455
"small",
5556
],
56-
suffix = "_stack_trace_%s" % include_stack_trace_option,
57+
suffix = "_stack_trace_%s_use_experimental_method_handles_%s" % (include_stack_trace_option, use_experimental_method_handles_option),
5758
deps = [":tests"],
5859
) for include_stack_trace_option in [
5960
"OFF",
6061
"ONLY_FOR_DECLARING_SOURCE",
62+
] for use_experimental_method_handles_option in [
63+
"NO",
64+
"YES",
6165
]]

core/test/com/google/inject/errors/NullInjectedIntoNonNullableTest.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.google.inject.errors;
22

3+
import static com.google.common.truth.Truth.assertThat;
34
import static com.google.inject.errors.ErrorMessageTestUtils.assertGuiceErrorEqualsIgnoreLineNumber;
45
import static org.junit.Assert.assertThrows;
56
import static org.junit.Assume.assumeTrue;
@@ -10,6 +11,7 @@
1011
import com.google.inject.Module;
1112
import com.google.inject.Provides;
1213
import com.google.inject.ProvisionException;
14+
import com.google.inject.Singleton;
1315
import com.google.inject.internal.InternalFlags;
1416
import com.google.inject.internal.InternalFlags.IncludeStackTraceOption;
1517
import java.lang.annotation.Retention;
@@ -60,7 +62,7 @@ protected void configure() {
6062
try {
6163
field = IntermediateModule.class.getField("NULL");
6264
} catch (NoSuchFieldException error) {
63-
throw new AssertionError("NULL field missing!");
65+
throw new AssertionError("NULL field missing!", error);
6466
}
6567
binder()
6668
.withSource(field)
@@ -110,4 +112,31 @@ public void nullReturnedFromProviderWithModuleStack() {
110112
assertGuiceErrorEqualsIgnoreLineNumber(
111113
exception.getMessage(), "null_returned_from_provider_with_module_stack.txt");
112114
}
115+
116+
// Ensure we report the error when the dependency is a singleton, no matter how many times we
117+
// try to provision it.
118+
@Test
119+
public void nullReturnedFromSingletonBinding() {
120+
Injector injector =
121+
Guice.createInjector(
122+
new AbstractModule() {
123+
@Override
124+
protected void configure() {
125+
bind(String.class)
126+
.annotatedWith(Bar.class)
127+
.toProvider(() -> null)
128+
.in(Singleton.class);
129+
}
130+
131+
@Provides
132+
String provideString(@Bar String string) {
133+
return string;
134+
}
135+
});
136+
var pe = assertThrows(ProvisionException.class, () -> injector.getInstance(String.class));
137+
assertThat(pe).hasMessageThat().contains("[Guice/NullInjectedIntoNonNullable]");
138+
139+
pe = assertThrows(ProvisionException.class, () -> injector.getInstance(String.class));
140+
assertThat(pe).hasMessageThat().contains("[Guice/NullInjectedIntoNonNullable]");
141+
}
113142
}

0 commit comments

Comments
 (0)