From df99123c4a4e29e951a8a32d325c4b4df098ace3 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Mon, 23 Oct 2023 10:56:09 -0700 Subject: [PATCH] Extract a helper class for constructing lambdas from TemplateCompiler This will soon be reused in a few different places. Add a tiny optimization for lambdas with zero bound parameters by compiling them to an `ldc` instruction. This special case happens to satisfy all of our current usecases and will satisfy a subset of the upcoming ones. This should be a tiny bootstraping and codesize improvement. PiperOrigin-RevId: 575868161 --- .../template/soy/jbcsrc/TemplateCompiler.java | 30 +--- .../soy/jbcsrc/restricted/LambdaFactory.java | 162 ++++++++++++++++++ .../soy/jbcsrc/restricted/MethodRef.java | 4 + .../shared/ExtraConstantBootstraps.java | 26 +++ .../jbcsrc/restricted/LambdaFactoryTest.java | 109 ++++++++++++ 5 files changed, 303 insertions(+), 28 deletions(-) create mode 100644 java/src/com/google/template/soy/jbcsrc/restricted/LambdaFactory.java create mode 100644 java/tests/com/google/template/soy/jbcsrc/restricted/LambdaFactoryTest.java diff --git a/java/src/com/google/template/soy/jbcsrc/TemplateCompiler.java b/java/src/com/google/template/soy/jbcsrc/TemplateCompiler.java index 45b9d07cb1..1013cb2005 100644 --- a/java/src/com/google/template/soy/jbcsrc/TemplateCompiler.java +++ b/java/src/com/google/template/soy/jbcsrc/TemplateCompiler.java @@ -41,6 +41,7 @@ import com.google.template.soy.jbcsrc.restricted.CodeBuilder; import com.google.template.soy.jbcsrc.restricted.Expression; import com.google.template.soy.jbcsrc.restricted.FieldRef; +import com.google.template.soy.jbcsrc.restricted.LambdaFactory; import com.google.template.soy.jbcsrc.restricted.LocalVariable; import com.google.template.soy.jbcsrc.restricted.MethodRef; import com.google.template.soy.jbcsrc.restricted.MethodRefs; @@ -66,7 +67,6 @@ import com.google.template.soy.types.NullType; import com.google.template.soy.types.TemplateType; import com.google.template.soy.types.TemplateType.Parameter; -import java.lang.invoke.LambdaMetafactory; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -153,18 +153,6 @@ void compile() { generateModifiableSelectMethod(); } - private static final Handle METAFACTORY_HANDLE = - MethodRef.createPure( - LambdaMetafactory.class, - "metafactory", - MethodHandles.Lookup.class, - String.class, - MethodType.class, - MethodType.class, - MethodHandle.class, - MethodType.class) - .asHandle(); - private static final Handle DELEGATE_FACTORY_HANDLE = MethodRef.createPure( RecordToPositionalCallFactory.class, @@ -176,9 +164,6 @@ void compile() { String[].class) .asHandle(); - private static final String COMPILED_TEMPLATE_INIT_DESCRIPTOR = - Type.getMethodDescriptor(BytecodeUtils.COMPILED_TEMPLATE_TYPE); - private static final Type COMPILED_TEMPLATE_RENDER_DESCRIPTOR = Type.getMethodType( BytecodeUtils.RENDER_RESULT_TYPE, @@ -203,18 +188,7 @@ private void generateTemplateMethod(MethodRef templateMethod, MethodRef renderMe // assuming foo is the name of the template class. Statement methodBody = Statement.returnExpression( - new Expression(BytecodeUtils.COMPILED_TEMPLATE_TYPE) { - @Override - protected void doGen(CodeBuilder cb) { - cb.visitInvokeDynamicInsn( - "render", - COMPILED_TEMPLATE_INIT_DESCRIPTOR, - METAFACTORY_HANDLE, - COMPILED_TEMPLATE_RENDER_DESCRIPTOR, - renderMethod.asHandle(), - COMPILED_TEMPLATE_RENDER_DESCRIPTOR); - } - }); + LambdaFactory.create(MethodRefs.COMPILED_TEMPLATE_RENDER, renderMethod).invoke()); CodeBuilder methodWriter = new CodeBuilder(methodAccess(), templateMethod.method(), /* exceptions= */ null, writer); generateTemplateMetadata(methodWriter); diff --git a/java/src/com/google/template/soy/jbcsrc/restricted/LambdaFactory.java b/java/src/com/google/template/soy/jbcsrc/restricted/LambdaFactory.java new file mode 100644 index 0000000000..d1b06610ff --- /dev/null +++ b/java/src/com/google/template/soy/jbcsrc/restricted/LambdaFactory.java @@ -0,0 +1,162 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.template.soy.jbcsrc.restricted; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Arrays.asList; + +import com.google.common.collect.ImmutableList; +import com.google.template.soy.jbcsrc.restricted.Expression.Feature; +import com.google.template.soy.jbcsrc.restricted.Expression.Features; +import com.google.template.soy.jbcsrc.shared.ExtraConstantBootstraps; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import org.objectweb.asm.ConstantDynamic; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Type; + +/** + * A helper for generating lambda callsites. Allows for some extra compile time checking and + * encapsulation. + */ +public final class LambdaFactory { + + private static final Handle METAFACTORY_HANDLE = + MethodRef.createPure( + LambdaMetafactory.class, + "metafactory", + MethodHandles.Lookup.class, + String.class, + MethodType.class, + MethodType.class, + MethodHandle.class, + MethodType.class) + .asHandle(); + private static final Handle CONDY_METAFACTORY = + MethodRef.createPure( + ExtraConstantBootstraps.class, + "constantMetafactory", + MethodHandles.Lookup.class, + String.class, + Class.class, + MethodType.class, + MethodHandle.class, + MethodType.class) + .asHandle(); + + /** + * Create a {@code LambdaFactory} that can create instances of an interface that implements {@code + * interfaceMethod} by delegating to {@code implMethod}. + */ + public static LambdaFactory create(MethodRef interfaceMethod, MethodRef implMethod) { + checkState(interfaceMethod.isInterfaceMethod(), "Only interfaces are supported"); + return new LambdaFactory(interfaceMethod.owner(), interfaceMethod, implMethod); + } + + private final TypeInfo interfaceType; + private final String interfaceMethodName; + private final Type interfaceMethodType; + private final MethodRef implMethod; + private final String callSiteDescriptor; + private final ImmutableList boundParams; + + private LambdaFactory(TypeInfo interfaceType, MethodRef interfaceMethod, MethodRef implMethod) { + this.interfaceType = interfaceType; + this.interfaceMethodName = interfaceMethod.method().getName(); + this.implMethod = implMethod; + // Skip the first param as it is the receiver type. MethodRef models 'receivers' as an argument + // but the jdk models them separately so from the perspective of the lambda-metafactory it isn't + // part of the interface method signature. + var interfaceMethodParams = + interfaceMethod.argTypes().subList(1, interfaceMethod.argTypes().size()); + this.interfaceMethodType = + Type.getMethodType( + interfaceMethod.returnType(), interfaceMethodParams.toArray(new Type[0])); + // Lambdas are essentially an inner classes + // class Impl extends Interface { + // Impl(...boundParams) {...} + // method(..freeParams) { return target(..boundParams, ...freeParams);} + // } + // So all 'free' params must be in trailing argument position of the target method and exactly + // match the interface method parameter types. + // The JDK does allow for there to be some mismatch, as long as the mismatches are resolvable + // with simple type coercion rules (aka if the java cast operator could handle it, so could the + // lambda factory). However, for now we require an exact match. + var trailingImplParams = + implMethod + .argTypes() + .subList( + implMethod.argTypes().size() - interfaceMethodParams.size(), + implMethod.argTypes().size()); + checkArgument( + interfaceMethodParams.equals(trailingImplParams), + "trailing parameters of %s must match the free parameters of %s", + implMethod, + interfaceMethod); + this.boundParams = + implMethod + .argTypes() + .subList(0, implMethod.argTypes().size() - interfaceMethodParams.size()); + this.callSiteDescriptor = + Type.getMethodDescriptor(interfaceType.type(), boundParams.toArray(new Type[0])); + } + + public Expression invoke(Expression... args) { + return invoke(asList(args)); + } + + public Expression invoke(Iterable args) { + Expression.checkTypes(boundParams, args); + var features = Features.of(Feature.NON_JAVA_NULLABLE, Feature.NON_SOY_NULLISH); + if (Expression.areAllCheap(args)) { + features = features.plus(Feature.CHEAP); + } + return new Expression(interfaceType.type(), features) { + @Override + protected void doGen(CodeBuilder cb) { + // When there are no bound parameters we can link this with condy instead of indy. + // According to Brian Goetz this should be faster to link even if the impls are identical + // See https://mail.openjdk.org/pipermail/amber-dev/2023-October/008327.html + if (boundParams.isEmpty()) { + cb.visitLdcInsn( + new ConstantDynamic( + interfaceMethodName, + interfaceType.type().getDescriptor(), + CONDY_METAFACTORY, + interfaceMethodType, + implMethod.asHandle(), + interfaceMethodType)); + + } else { + for (Expression arg : args) { + arg.gen(cb); + } + cb.visitInvokeDynamicInsn( + interfaceMethodName, + callSiteDescriptor, + METAFACTORY_HANDLE, + interfaceMethodType, + implMethod.asHandle(), + interfaceMethodType); + } + } + }; + } +} diff --git a/java/src/com/google/template/soy/jbcsrc/restricted/MethodRef.java b/java/src/com/google/template/soy/jbcsrc/restricted/MethodRef.java index 0d42ad72ee..d2c5d63f09 100644 --- a/java/src/com/google/template/soy/jbcsrc/restricted/MethodRef.java +++ b/java/src/com/google/template/soy/jbcsrc/restricted/MethodRef.java @@ -248,6 +248,10 @@ public Handle asHandle() { owner().isInterface()); } + boolean isInterfaceMethod() { + return owner().isInterface() && opcode() == Opcodes.INVOKEINTERFACE; + } + // TODO(lukes): consider different names. 'invocation'? invoke() makes it sounds like we are // actually calling the method rather than generating an expression that will output code that // will invoke the method. diff --git a/java/src/com/google/template/soy/jbcsrc/shared/ExtraConstantBootstraps.java b/java/src/com/google/template/soy/jbcsrc/shared/ExtraConstantBootstraps.java index 18ec9dd87e..f7a4570141 100644 --- a/java/src/com/google/template/soy/jbcsrc/shared/ExtraConstantBootstraps.java +++ b/java/src/com/google/template/soy/jbcsrc/shared/ExtraConstantBootstraps.java @@ -16,6 +16,7 @@ package com.google.template.soy.jbcsrc.shared; import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.lang.invoke.MethodType.methodType; import static java.util.Arrays.stream; import com.google.common.collect.ImmutableList; @@ -27,7 +28,10 @@ import com.google.template.soy.data.internal.ParamStore; import com.google.template.soy.data.internal.SoyMapImpl; import com.google.template.soy.data.internal.SoyRecordImpl; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; /** Extra constant bootstrap methods. */ public final class ExtraConstantBootstraps { @@ -100,5 +104,27 @@ public static RecordProperty symbol(MethodHandles.Lookup lookup, String name, Cl return RecordProperty.get(name); } + // JDK has half implemented support for invoking lambdas via condy. + // There is a special case for this exact signature + // See BootstrapMethodInvoker+ isLambdaMetafactoryCondyBSM + // and https://bugs.openjdk.org/browse/JDK-8198418 + // According to Brian Goetz this got de-staffed for priority and scope creep reasons but is still + // a good idea because condy linkage is cheaper than invokedynamic + // https://mail.openjdk.org/pipermail/amber-dev/2023-October/008327.html + @Keep + public static Object constantMetafactory( + MethodHandles.Lookup lookup, + String name, + Class type, + MethodType samMethodType, + MethodHandle implMethod, + MethodType instantiatedMethodType) + throws Throwable { + return LambdaMetafactory.metafactory( + lookup, name, methodType(type), samMethodType, implMethod, instantiatedMethodType) + .getTarget() + .invoke(); + } + private ExtraConstantBootstraps() {} } diff --git a/java/tests/com/google/template/soy/jbcsrc/restricted/LambdaFactoryTest.java b/java/tests/com/google/template/soy/jbcsrc/restricted/LambdaFactoryTest.java new file mode 100644 index 0000000000..1f5c4990ce --- /dev/null +++ b/java/tests/com/google/template/soy/jbcsrc/restricted/LambdaFactoryTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.template.soy.jbcsrc.restricted; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.template.soy.jbcsrc.restricted.BytecodeUtils.constant; + +import com.google.template.soy.jbcsrc.restricted.testing.ExpressionEvaluator; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class LambdaFactoryTest { + // Interface methods to implement with a lambda + private static final MethodRef SUPPLIER_GET = MethodRef.createPure(Supplier.class, "get"); + private static final MethodRef FUNCTION_APPLY = + MethodRef.createPure(Function.class, "apply", Object.class); + private static final MethodRef BIFUNCTION_APPLY = + MethodRef.createPure(BiFunction.class, "apply", Object.class, Object.class); + + // Implementation referecences + private static final MethodRef IDENTITY_FUNCTION = + MethodRef.createPure(LambdaFactoryTest.class, "identity", Object.class); + private static final MethodRef RETURNS_HELLO_FUNCTION = + MethodRef.createPure(LambdaFactoryTest.class, "returnsHello"); + private static final MethodRef CONCAT_FUNCTION = + MethodRef.createPure(LambdaFactoryTest.class, "concat", Object.class, Object.class); + + // Actual implementations + public static Object identity(Object o) { + return o; + } + + public static Object concat(Object o, Object o2) { + return o + ", " + o2; + } + + public static Object returnsHello() { + return "hello"; + } + + @Test + public void testNoBoundParameters() throws ReflectiveOperationException { + @SuppressWarnings("unchecked") + Supplier supplier = + (Supplier) + ExpressionEvaluator.evaluate( + LambdaFactory.create(SUPPLIER_GET, RETURNS_HELLO_FUNCTION).invoke()); + + assertThat(supplier.get()).isEqualTo("hello"); + } + + @Test + public void testOneBundParameter() throws ReflectiveOperationException { + @SuppressWarnings("unchecked") + Supplier supplier = + (Supplier) + ExpressionEvaluator.evaluate( + LambdaFactory.create(SUPPLIER_GET, IDENTITY_FUNCTION).invoke(constant("bound"))); + assertThat(supplier.get()).isEqualTo("bound"); + } + + @Test + public void testOneFreeParameter() throws ReflectiveOperationException { + @SuppressWarnings("unchecked") + Function fn = + (Function) + ExpressionEvaluator.evaluate( + LambdaFactory.create(FUNCTION_APPLY, IDENTITY_FUNCTION).invoke()); + assertThat(fn.apply("free")).isEqualTo("free"); + } + + @Test + public void testOneFreeAndOneBoundParameter() throws ReflectiveOperationException { + @SuppressWarnings("unchecked") + Function fn = + (Function) + ExpressionEvaluator.evaluate( + LambdaFactory.create(FUNCTION_APPLY, CONCAT_FUNCTION).invoke(constant("first"))); + assertThat(fn.apply("second")).isEqualTo("first, second"); + } + + @Test + public void testTwoFreeParameters() throws ReflectiveOperationException { + @SuppressWarnings("unchecked") + BiFunction fn = + (BiFunction) + ExpressionEvaluator.evaluate( + LambdaFactory.create(BIFUNCTION_APPLY, CONCAT_FUNCTION).invoke()); + assertThat(fn.apply("first", "second")).isEqualTo("first, second"); + } +}