Skip to content

Lambda serialization does not create required bridge methods for SAM types #12418

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

Open
retronym opened this issue Jun 21, 2021 · 0 comments
Open
Labels
Milestone

Comments

@retronym
Copy link
Member

reproduction steps

using Scala 2.13.6

import java.io.{ByteArrayInputStream, ByteArrayOutputStream, ObjectInputStream, ObjectOutputStream}
import java.lang.invoke.{MethodHandleInfo, SerializedLambda}

import scala.tools.nsc.util

trait F1[-A, +B] {
  def apply(a: A): B
}
trait StringF1[+B] extends F1[String, B] with java.io.Serializable {
  def apply(s: String): B
}

class C extends java.io.Serializable {
  val sf1s = List[StringF1[String]](
    (s) => s.reverse,
    (s) => s.reverse,
  )
  @annotation.unused private def foo(): Unit = {
    assert(false, "should not be called!!!")
  }
}

object Test {
  def main(args: Array[String]): Unit = {
    allRealLambdasRoundTrip()
  }

  def allRealLambdasRoundTrip(): Unit = {
    new C().sf1s.map(x => { 
      val y = serializeDeserialize(x)
      y.apply("foo");
      def applyF1[A, B](f: F1[A, B])(a: A): B = f(a)
      applyF1(y)("foo")
    })
  }

  def serializeDeserialize[T <: AnyRef](obj: T) = {
    val buffer = new ByteArrayOutputStream
    val out = new ObjectOutputStream(buffer)
    out.writeObject(obj)
    val in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray))
    in.readObject.asInstanceOf[T]
  }
}

problem

Exception in thread "main" java.lang.AbstractMethodError: Receiver class C$$Lambda$569/0x0000000800346c40 does not define or inherit an implementation of the resolved method 'abstract java.lang.Object apply(java.lang.Object)' of interface F1.
	at Test$.applyF1$1(lambda-serialization.scala:53)
	at Test$.$anonfun$allRealLambdasRoundTrip$2(lambda-serialization.scala:54)
	at scala.collection.immutable.List.map(List.scala:246)

notes

To suppose lambda deserialiation, we add a synthetic $deserializeLambda$ method to each class that hosts lambda implementation methods. The JDK routes deserialization through this method. It checks that the request actually targets a method that backs a lambda (and not some random, maybe private method), by checking in a statically contructed map of all impl methods. If so, the lambda metatafactory is invoked. The metafactory invokation lives, generically, in scala.runtime.LambdaDeserializer. It does not correctly replicate the invokedynamic call at the lambda capture site as it does not include bridges.

LambdaDeserializer itself is a workaround for a problem we hit when we directly replicated the Java's strategy of simply having an invoke-dynamic-call per-lamnbda in the $deserializeLambda$. Because Scala lambdas are serializable by default, and lambda use is widespread, this method got very large, especially in DSL-like code. The large method was a problem in that it could be get too large to JIT or even too large to fit in a single method.

Maybe we can switch to a hybrid strategy where bridged lambdas have direct invokedynamic calls and scala.FunctionN cojntinue to use LambdaDeserializer. This will be cross-compatible with newly compiled user-code and the exising versions of scala.runtime.

@SethTisue SethTisue added this to the 2.13.7 milestone Jun 21, 2021
@SethTisue SethTisue modified the milestones: 2.13.7, 2.13.8 Oct 21, 2021
@dwijnand dwijnand modified the milestones: 2.13.8, Backlog Nov 18, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants