Skip to content

Add a generic deserializer for Java/Scala 2.12 lambdas #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 22, 2015

Conversation

retronym
Copy link
Member

Java support serialization of lambdas by using the serialization
proxy pattern. Deserialization of a lambda uses LambdaMetafactory
to create a new anonymous subclass.

More details of the scheme are documented:

https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/SerializedLambda.html

From those docs:

SerializedLambda has a readResolve method that looks for a
(possibly private) static method called $deserializeLambda$
in the capturing class, invokes that with itself as the first
argument, and returns the result. Lambda classes implementing
$deserializeLambda$ are responsible for validating that the
properties of the SerializedLambda are consistent with a lambda
actually captured by that class.

The Java compiler generates code in $deserializeLambda$ that
switches on the implementation method name and signature to locate
an invokedynamic instruction generated for the particular lambda
expression. Then, the SerializedLambda is further unpacked,
validating that this implementation method still represents the
same functional interface as it did when it was serialized.
(The source may have been recompiled in the interim.)

In Java, serializable lambda expressions are the exception rather than
the rule. In Scala, however, the serializability of FunctionN means
that we would end up generating a large amount of code to support
deserialization.

Instead, we are pursuing an alternative approach in which the
$deserializeLambda$ method is a simple forwarder to the generic
deserializer added here.

This is capable of deserializing lambdas created by the Java compiler,
although this is not its intended use case. The enclosed tests use
Java lambdas.

This generic deserializer also works by calling LambdaMetafactory,
but it does so explicitly, rather than implicitly during linkage
of the invokedynamic instruction.

We have to mimic the caching property of invokedynamic instruction
to ensure we reuse the classes when constructing. The cache here
uses weak references to keys and values to avoid retention of Class
or ClassLoader instances.

If the name or signature of the implementation method has changed,
we fail during deserialization with an IllegalArgumentError.

However, we do not fail fast in a few cases that Java would, as we
cannot reflect on the "current" functional interface supported by
this implementation method. We just instantiate using the "previous"
functional interface class/method.

This might:

  1. fail inside LambdaMetafactory if the new implementation
    method is not compatible with the old functional interface.
  2. pass through LambdaMetafactory by chance, but fail
    when instantiating the class in other cases. For example:
% tail sandbox/test{1,2}.scala
==> sandbox/test1.scala <==
class C {
  def test: (String => String) = {
    val s: String = ""
    (t) => s + t
  }
}

==> sandbox/test2.scala <==
class C {
  def test: (String, String) => String = {
    (s, t) => s + t
  }
}
% (for i in 1 2; do scalac -Ydelambdafy:method -Xprint:delambdafy sandbox/test$i.scala 2>&1 ; done) | grep 'def $anon'
    final <static> <artifact> private[this] def $anonfun$1(t: String, s$1: String): String = s$1.+(t);
    final <static> <artifact> private[this] def $anonfun$1(s: String, t: String): String = s.+(t);
  1. Silently create an instance of the old functional interface.
    For example, imagine switching from FuncInterface1 to
    FuncInterface2 where these were identical other than the name.

I don't believe that these are showstoppers.

@retronym
Copy link
Member Author

Review by @lrytz

* not stored in `SerializedLambda`, so we can't reconstitute them.
* - No additional bridge methods are passed to `altMetafactory`. Again, these are not stored.
*
* Note: The Java compiler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incomplete ℹ️

@lrytz
Copy link
Member

lrytz commented May 13, 2015

The code and the approach LGTM, really nice.

@retronym retronym force-pushed the topic/lambda-deserialize branch from 8c5d4ee to 6472976 Compare May 14, 2015 06:00
@retronym
Copy link
Member Author

Ready for re-review, @lrytz

retronym added a commit to retronym/scala that referenced this pull request May 15, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

[1] scala/scala-java8-compat#37
retronym added a commit to retronym/scala that referenced this pull request May 15, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
retronym added a commit to retronym/scala that referenced this pull request May 15, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
retronym added a commit to retronym/scala that referenced this pull request May 15, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
* This class is only intended to be called by synthetic `$deserializeLambda$` method that the Scala 2.12
* compiler will add to classes hosting lambdas.
*
* It is intended to be consumed directly.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what message this phrase is conveying :) - or does it miss a "not"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct 😄

@retronym
Copy link
Member Author

Yep, that's right. Note that JFUnctionN doesn't implement Serializable, either.

@retronym
Copy link
Member Author

s/faithfully unknown/faithfully deserialize unknown/ above.

@retronym
Copy link
Member Author

@retronym
Copy link
Member Author

One thing we could consider is synthesizing a static cache per-class. We could then reason about the object lifetimes by analogy to the reflection caches for structural type invocations.

retronym added a commit to retronym/scala that referenced this pull request May 16, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class with lambdas. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
retronym added a commit to retronym/scala that referenced this pull request May 16, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class with lambdas. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% qscala $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
retronym added a commit to retronym/scala that referenced this pull request May 16, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class with lambdas. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% qscala $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
retronym added a commit to retronym/scala that referenced this pull request May 16, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class with lambdas. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% qscala $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
retronym added a commit to retronym/scala that referenced this pull request May 16, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class with lambdas. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` can reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run. But first we'll need to host a cache in a static field
of each lambda hosting class. This is noted as a TODO and a failing
test, and will be updated in the next commit.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% qscala $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
retronym added a commit to retronym/scala that referenced this pull request May 17, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class with lambdas. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` can reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run. But first we'll need to host a cache in a static field
of each lambda hosting class. This is noted as a TODO and a failing
test, and will be updated in the next commit.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% qscala $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
retronym added a commit to retronym/scala that referenced this pull request May 17, 2015
To support serialization, we use the alternative lambda metafactory
that lets us specify that our anonymous functions should extend the
marker interface `scala.Serializable`. They will also have a
`writeObject` method added that implements the serialization proxy
pattern using `j.l.invoke.SerializedLamba`.

To support deserialization, we synthesize a `$deserializeLamba$`
method in each class with lambdas. This will be called reflectively by
`SerializedLambda#readResolve`. This method in turn delegates to
`LambdaDeserializer`, currently defined [1] in `scala-java8-compat`,
that uses `LambdaMetafactory` to spin up the anonymous class and
instantiate it with the deserialized environment.

Note: `LambdaDeserializer` can reuses the anonymous class on subsequent
deserializations of a given lambda, in the same spirit as an
invokedynamic call site only spins up the class on the first time
it is run. But first we'll need to host a cache in a static field
of each lambda hosting class. This is noted as a TODO and a failing
test, and will be updated in the next commit.

`LambdaDeserializer` will be moved into our standard library in
the 2.12.x branch, where we can introduce dependencies on the
Java 8 standard library.

The enclosed test cases must be manually run with indylambda enabled.
Once we enable indylambda by default on 2.12.x, the test will
actually test the new feature.

```
% echo $INDYLAMBDA
-Ydelambdafy:method -Ybackend:GenBCode -target:jvm-1.8 -classpath .:scala-java8-compat_2.11-0.5.0-SNAPSHOT.jar
% qscala $INDYLAMBDA -e "println((() => 42).getClass)"
class Main$$anon$1$$Lambda$1/1183231938
% qscala $INDYLAMBDA -e "assert(classOf[scala.Serializable].isInstance(() => 42))"
% qscalac $INDYLAMBDA test/files/run/lambda-serialization.scala && qscala $INDYLAMBDA Test
```

This commit contains a few minor refactorings to the code that
generates the invokedynamic instruction to use more meaningful
names and to reuse Java signature generation code in ASM rather
than the DIY approach.

[1] scala/scala-java8-compat#37
Java support serialization of lambdas by using the serialization
proxy pattern. Deserialization of a lambda uses `LambdaMetafactory`
to create a new anonymous subclass.

More details of the scheme are documented:

  https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/SerializedLambda.html

From those docs:

> SerializedLambda has a readResolve method that looks for a
> (possibly private) static method called $deserializeLambda$
> in the capturing class, invokes that with itself as the first
> argument, and returns the result. Lambda classes implementing
> $deserializeLambda$ are responsible for validating that the
> properties of the SerializedLambda are consistent with a lambda
> actually captured by that class.

The Java compiler generates code in `$deserializeLambda$` that
switches on the implementation method name and signature to locate
an invokedynamic instruction generated for the particular lambda
expression. Then, the `SerializedLambda` is further unpacked,
validating that this implementation method still represents the
same functional interface as it did when it was serialized.
(The source may have been recompiled in the interim.)

In Java, serializable lambda expressions are the exception rather than
the rule. In Scala, however, the serializability of `FunctionN` means
that we would end up generating a large amount of code to support
deserialization.

Instead, we are pursuing an alternative approach in which the
`$deserializeLambda$` method is a simple forwarder to the generic
deserializer added here.

This is capable of deserializing lambdas created by the Java compiler,
although this is not its intended use case. The enclosed tests use
Java lambdas.

This generic deserializer also works by calling `LambdaMetafactory`,
but it does so explicitly, rather than implicitly during linkage
of the `invokedynamic` instruction.

We have to mimic the caching property of `invokedynamic` instruction
to ensure we reuse the classes when constructing. I originally tried
using a central cache, but wasn't able to come up with a scheme to
avoid potential classloader memory leaks. Instead, I now allow
the caller to provide a cache. The scala compiler will host an
instance of this cache in each class that hosts a lambda. This is
analagous the the `MethodCache` used by reflective calls.

If the name or signature of the implementation method has changed,
we fail during deserialization with an `IllegalArgumentError.`

However, we do not fail fast in a few cases that Java would, as we
cannot reflect on the "current" functional interface supported by
this implementation method. We just instantiate using the "previous"
functional interface class/method.

This might:

1. fail inside `LambdaMetafactory` if the new implementation
method is not compatible with the old functional interface.

2. pass through `LambdaMetafactory` by chance, but fail
when instantiating the class in other cases. For example:

```
% tail sandbox/test{1,2}.scala
==> sandbox/test1.scala <==
class C {
  def test: (String => String) = {
    val s: String = ""
    (t) => s + t
  }
}

==> sandbox/test2.scala <==
class C {
  def test: (String, String) => String = {
    (s, t) => s + t
  }
}
% (for i in 1 2; do scalac -Ydelambdafy:method -Xprint:delambdafy sandbox/test$i.scala 2>&1 ; done) | grep 'def $anon'
    final <static> <artifact> private[this] def $anonfun$1(t: String, s$1: String): String = s$1.+(t);
    final <static> <artifact> private[this] def $anonfun$1(s: String, t: String): String = s.+(t);
```

3. Silently create an instance of the old functional interface.
For example, imagine switching from `FuncInterface1` to
`FuncInterface2` where these were identical other than the name.

I don't believe that these are showstoppers.

Failing test case demonstrating overly weak cache
LambdaMetafactory returns a ConstantCallSite bound to a shared instance
of a lambda, rather than a reference to the no-arg constructor. This is a
technique to avoid unnecessary allocations.

This test checks that we preserve this property when deserializing.
@retronym retronym force-pushed the topic/lambda-deserialize branch from 6472976 to 921b212 Compare May 20, 2015 23:06
@lrytz
Copy link
Member

lrytz commented May 21, 2015

LGTM!

retronym added a commit that referenced this pull request May 22, 2015
Add a generic deserializer for Java/Scala 2.12 lambdas
@retronym retronym merged commit aa0908b into scala:master May 22, 2015
@retronym retronym added this to the 0.5.0 milestone May 22, 2015
@retronym retronym modified the milestones: 0.5.0, 0.6.0 Aug 13, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants