aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar janakr <janakr@google.com>2018-06-13 23:07:15 -0700
committerGravatar Copybara-Service <copybara-piper@google.com>2018-06-13 23:08:56 -0700
commit5eae3409e1b60b803c4a7cdf1dd890af03346b33 (patch)
tree9d36eae6959d9df149dc74a4da32b7154e92d379
parent4343fc6ccab7bcdd9e8cd9035903da656b5c18df (diff)
Serialize lambdas when they are cast to Serializable.
We could conceivably do some monkey-patching at server startup to make all lambdas act Serializable, but one step at a time. PiperOrigin-RevId: 200509321
-rw-r--r--src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java9
-rw-r--r--src/main/java/com/google/devtools/build/lib/skyframe/serialization/AutoRegistry.java5
-rw-r--r--src/main/java/com/google/devtools/build/lib/skyframe/serialization/LambdaCodec.java93
-rw-r--r--src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodec.java5
-rw-r--r--src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecRegistry.java37
-rw-r--r--src/test/java/com/google/devtools/build/lib/skyframe/serialization/LambdaCodecTest.java72
6 files changed, 202 insertions, 19 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
index d78ba32791..de3d34ebcf 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
@@ -82,6 +82,7 @@ import com.google.devtools.build.lib.rules.java.ProguardHelper.ProguardOutput;
import com.google.devtools.build.lib.rules.java.ProguardSpecProvider;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -1373,13 +1374,7 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory {
.setExecutable(ruleContext.getExecutablePrerequisite("$dexmerger", Mode.HOST))
.setMnemonics("DexShardsToMerge", "DexMerger")
.setOutputPathMapper(
- // Use an anonymous inner class for serialization.
- new OutputPathMapper() {
- @Override
- public PathFragment parentRelativeOutputPath(TreeFileArtifact input) {
- return input.getParentRelativePath();
- }
- });
+ (OutputPathMapper & Serializable) TreeFileArtifact::getParentRelativePath);
CustomCommandLine.Builder commandLine =
CustomCommandLine.builder()
.addPlaceholderTreeArtifactExecPath("--input", inputTree)
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/AutoRegistry.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/AutoRegistry.java
index 87be12e504..c4e880881e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/AutoRegistry.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/AutoRegistry.java
@@ -46,7 +46,10 @@ public class AutoRegistry {
/** Classes outside {@link AutoRegistry#PACKAGE_PREFIX} that need to be serialized. */
private static final ImmutableList<String> EXTERNAL_CLASS_NAMES_TO_REGISTER =
- ImmutableList.of("java.io.FileNotFoundException", "java.io.IOException");
+ ImmutableList.of(
+ "java.io.FileNotFoundException",
+ "java.io.IOException",
+ "java.lang.invoke.SerializedLambda");
private static final ImmutableList<Object> REFERENCE_CONSTANTS_TO_REGISTER =
ImmutableList.of(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/LambdaCodec.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/LambdaCodec.java
new file mode 100644
index 0000000000..1127308ca9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/LambdaCodec.java
@@ -0,0 +1,93 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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.devtools.build.lib.skyframe.serialization;
+
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.invoke.SerializedLambda;
+import java.lang.reflect.Method;
+
+/**
+ * A codec for Java 8 serializable lambdas. Lambdas that are tagged as {@link Serializable} have a
+ * generated method, {@code writeReplace}, that converts them into a {@link SerializedLambda}, which
+ * can then be serialized like a normal object. On deserialization, we call {@link
+ * SerializedLambda#readResolve}, which converts the object back into a lambda.
+ *
+ * <p>Since lambdas do not share a common base class, choosing this codec for serializing them must
+ * be special-cased in {@link ObjectCodecRegistry}. We must also make a somewhat arbitrary choice
+ * around the generic parameter. Since all of our lambdas are {@link Serializable}, we use that.
+ * Because {@link Serializable} is an interface, not a class, this codec will never be chosen for
+ * any object without special-casing.
+ */
+class LambdaCodec implements ObjectCodec<Serializable> {
+ private final Method readResolveMethod;
+
+ LambdaCodec() {
+ try {
+ this.readResolveMethod = SerializedLambda.class.getDeclaredMethod("readResolve");
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException(e);
+ }
+ readResolveMethod.setAccessible(true);
+ }
+
+ static boolean isProbablyLambda(Class<?> type) {
+ return type.isSynthetic() && !type.isLocalClass() && !type.isAnonymousClass();
+ }
+
+ @Override
+ public Class<? extends Serializable> getEncodedClass() {
+ return Serializable.class;
+ }
+
+ @Override
+ public void serialize(SerializationContext context, Serializable obj, CodedOutputStream codedOut)
+ throws SerializationException, IOException {
+ Class<?> objClass = obj.getClass();
+ if (!isProbablyLambda(objClass)) {
+ throw new SerializationException(obj + " is not a lambda: " + objClass);
+ }
+ Method writeReplaceMethod;
+ try {
+ // TODO(janakr): We could cache these methods if retrieval shows up as a hotspot.
+ writeReplaceMethod = objClass.getDeclaredMethod("writeReplace");
+ } catch (NoSuchMethodException e) {
+ throw new SerializationException(
+ "No writeReplace method for " + obj + " with " + objClass, e);
+ }
+ writeReplaceMethod.setAccessible(true);
+ SerializedLambda serializedLambda;
+ try {
+ serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(obj);
+ } catch (ReflectiveOperationException e) {
+ throw new SerializationException(
+ "Exception invoking writeReplace for " + obj + " with " + objClass, e);
+ }
+ context.serialize(serializedLambda, codedOut);
+ }
+
+ @Override
+ public Serializable deserialize(DeserializationContext context, CodedInputStream codedIn)
+ throws SerializationException, IOException {
+ SerializedLambda serializedLambda = context.deserialize(codedIn);
+ try {
+ return (Serializable) readResolveMethod.invoke(serializedLambda);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Error read-resolving " + serializedLambda, e);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodec.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodec.java
index ab004def86..7ab9de5959 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodec.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodec.java
@@ -30,6 +30,11 @@ public interface ObjectCodec<T> {
*
* <p>This is useful for automatically dispatching to the correct codec, e.g. in {@link
* ObjectCodecs}.
+ *
+ * <p>If {@link T} is an interface, then this codec will never be used by the auto-registration
+ * framework in {@link ObjectCodecRegistry} unless it is explicitly invoked or {@link
+ * #additionalEncodedClasses} is non-empty, since the {@link ObjectCodecRegistry} traverses the
+ * concrete class hierarchy looking for matches, and will never come to an interface.
*/
Class<? extends T> getEncodedClass();
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecRegistry.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecRegistry.java
index 2fb0ac39e7..7c28f6a581 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecRegistry.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecRegistry.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableSortedSet;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import java.io.IOException;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
@@ -106,7 +107,7 @@ public class ObjectCodecRegistry {
// Enums must be serialized using declaring class.
type = ((Enum) obj).getDeclaringClass();
}
- return getDynamicCodecDescriptor(type.getName());
+ return getDynamicCodecDescriptor(type.getName(), type);
}
/**
@@ -157,7 +158,7 @@ public class ObjectCodecRegistry {
if (!allowDefaultCodec || tagOffset < 0 || tagOffset >= classNames.size()) {
throw new SerializationException.NoCodecException("No codec available for tag " + tag);
}
- return getDynamicCodecDescriptor(classNames.get(tagOffset));
+ return getDynamicCodecDescriptor(classNames.get(tagOffset), /*type=*/ null);
}
/**
@@ -382,18 +383,32 @@ public class ObjectCodecRegistry {
return new TypedCodecDescriptor(tag, new EnumCodec(enumType));
}
- private CodecDescriptor getDynamicCodecDescriptor(String className)
+ private CodecDescriptor getDynamicCodecDescriptor(String className, @Nullable Class<?> type)
throws SerializationException.NoCodecException {
Supplier<CodecDescriptor> supplier = dynamicCodecs.get(className);
- if (supplier == null) {
- throw new SerializationException.NoCodecException(
- "No default codec available for " + className);
+ if (supplier != null) {
+ CodecDescriptor descriptor = supplier.get();
+ if (descriptor == null) {
+ throw new SerializationException.NoCodecException(
+ "There was a problem creating a codec for " + className + ". Check logs for details");
+ }
+ return descriptor;
}
- CodecDescriptor descriptor = supplier.get();
- if (descriptor == null) {
- throw new SerializationException.NoCodecException(
- "There was a problem creating a codec for " + className + " check logs for details.");
+ if (type != null && LambdaCodec.isProbablyLambda(type)) {
+ if (Serializable.class.isAssignableFrom(type)) {
+ // LambdaCodec is hidden away as a codec for Serializable. This avoids special-casing it in
+ // all places we look up a codec, and doesn't clash with anything else because Serializable
+ // is an interface, not a class.
+ return classMappedCodecs.get(Serializable.class);
+ } else {
+ throw new SerializationException.NoCodecException(
+ "No default codec available for "
+ + className
+ + ". If this is a lambda, try casting it to (type & Serializable), like "
+ + "(Supplier<String> & Serializable)");
+ }
}
- return descriptor;
+ throw new SerializationException.NoCodecException(
+ "No default codec available for " + className);
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/LambdaCodecTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/LambdaCodecTest.java
new file mode 100644
index 0000000000..4d987e2567
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/LambdaCodecTest.java
@@ -0,0 +1,72 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// 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.devtools.build.lib.skyframe.serialization;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
+
+import com.google.devtools.build.lib.skyframe.serialization.testutils.SerializationTester;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.TestUtils;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link LambdaCodec}. */
+@RunWith(JUnit4.class)
+public class LambdaCodecTest {
+ private interface MyInterface {
+ boolean func(String arg);
+ }
+
+ @Test
+ public void smoke() throws Exception {
+ List<Boolean> returnValue = new ArrayList<>();
+ new SerializationTester(
+ (Supplier<Void> & Serializable) () -> null,
+ (Function<Object, String> & Serializable) Object::toString,
+ (MyInterface & Serializable) arg -> "hello".equals(arg),
+ (MyInterface & Serializable) "hello"::equals,
+ (MyInterface & Serializable) (arg) -> !returnValue.isEmpty())
+ // We can't compare lambdas for equality, just make sure they get deserialized.
+ .setVerificationFunction((original, deserialized) -> assertThat(deserialized).isNotNull())
+ .runTests();
+ }
+
+ @Test
+ public void lambdaBehaviorPreserved() throws Exception {
+ List<Boolean> returnValue = new ArrayList<>();
+ MyInterface lambda = (MyInterface & Serializable) (arg) -> returnValue.isEmpty();
+ MyInterface deserializedLambda = TestUtils.roundTrip(lambda);
+ assertThat(lambda.func("any")).isTrue();
+ assertThat(deserializedLambda.func("any")).isTrue();
+ returnValue.add(true);
+ assertThat(lambda.func("any")).isFalse();
+ // Deserialized object's list is not the same as original's. Changes to original aren't seen.
+ assertThat(deserializedLambda.func("any")).isTrue();
+ }
+
+ @Test
+ public void onlySerializableWorks() {
+ MyInterface unserializableLambda = (arg) -> true;
+ assertThrows(
+ SerializationException.class,
+ () -> TestUtils.toBytesMemoized(unserializableLambda, AutoRegistry.get()));
+ }
+}