You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pekko.apache.org by he...@apache.org on 2026/07/09 13:08:58 UTC

(pekko-grpc) branch main updated: refactor: replace codegen reflection with method handles (#782)

This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-grpc.git


The following commit(s) were added to refs/heads/main by this push:
     new ede67241 refactor: replace codegen reflection with method handles (#782)
ede67241 is described below

commit ede672416bff1d06db574be9a32147570ba971bc
Author: He-Pin(kerr) <he...@gmail.com>
AuthorDate: Thu Jul 9 21:08:53 2026 +0800

    refactor: replace codegen reflection with method handles (#782)
    
    Motivation:
    Avoid reflective constructor, method, and Scala module invocation in gRPC code generation and plugin bridge paths while keeping generator loading compatible across Scala versions.
    
    Modification:
    Cache MethodHandles for logger forwarding and extra generator constructors. Use public MethodHandles for Scala singleton module access and generator bridge calls. Remove the Scala 2.12 ToolBox path in ReflectiveCodeGen and use fixed-arity invoke() call sites with explicit Object values where needed.
    
    Result:
    Codegen, Maven plugin, sbt plugin, and reflective test build generation use MethodHandles consistently while preserving public access semantics and avoiding avoidable invokeWithArguments boxing.
    
    Tests:
    - JDK 17: sbt "codegen / compile" "maven-plugin / compile" "sbt-plugin / compile" - success
    - JDK 17: sbt "++2.13.18!" codegen/test "++3.3.8!" codegen/test - success, 2 tests per version
    - JDK 17: sbt "++2.13.*" "sbt-plugin/scripted gen-scala-server/06-compatibility-plugins" - first failed because protoc-gen-go was missing; installed protoc-gen-go v1.32.0, rerun success
    - JDK 17: sbt "++3.8.4!" "sbt-plugin/scripted scala3/*" - success, 3 scripted tests
    - JDK 17: sbt "++2.13.*" "sbt-plugin/scripted gen-scala-server/06-compatibility-plugins" "++3.8.4!" "sbt-plugin/scripted scala3/03-sbt2-basic" - success after explicit Object annotations
    - scalafmt --mode diff-ref=origin/main - success
    - scalafmt --list --mode diff-ref=origin/main - success
    - git diff --check - success
    - Qoder stdout review: /tmp/project-qoder-review.log - No must-fix findings
    
    References:
    None - internal refactoring
---
 .../scala/org/apache/pekko/grpc/gen/Logging.scala  | 38 ++++++++---
 .../scala/org/apache/pekko/grpc/gen/Main.scala     | 18 +++++-
 .../pekko/grpc/maven/AbstractGenerateMojo.scala    | 18 +++++-
 project/ReflectiveCodeGen.scala                    | 75 +++++-----------------
 .../apache/pekko/grpc/sbt/GeneratorBridge.scala    | 30 +++++----
 5 files changed, 95 insertions(+), 84 deletions(-)

diff --git a/codegen/src/main/scala/org/apache/pekko/grpc/gen/Logging.scala b/codegen/src/main/scala/org/apache/pekko/grpc/gen/Logging.scala
index f5769371..68078608 100644
--- a/codegen/src/main/scala/org/apache/pekko/grpc/gen/Logging.scala
+++ b/codegen/src/main/scala/org/apache/pekko/grpc/gen/Logging.scala
@@ -14,8 +14,11 @@
 package org.apache.pekko.grpc.gen
 
 import java.io.PrintWriter
+import java.lang.invoke.{ MethodHandle, MethodHandles, MethodType => JMethodType }
 import java.nio.charset.StandardCharsets
 
+import scala.collection.concurrent.TrieMap
+
 // specific to gen so that the build tools can implement their own
 trait Logger {
   def debug(text: String): Unit
@@ -62,19 +65,36 @@ class FileLogger(path: String) extends Logger {
 }
 
 /**
- * Logger that forwards calls to another Logger via reflection.
+ * Logger that forwards calls to another Logger via MethodHandles.
  *
  *  This enables a code generator that is loaded inside a sandboxed class loader to
  *  use a logger that lives in a different class loader.
  */
 class ReflectiveLogger(logger: Object) extends Logger {
-  private val debugMethod = logger.getClass.getMethod("debug", classOf[String])
-  private val infoMethod = logger.getClass.getMethod("info", classOf[String])
-  private val warnMethod = logger.getClass.getMethod("warn", classOf[String])
-  private val errorMethod = logger.getClass.getMethod("error", classOf[String])
+  import ReflectiveLogger._
+
+  private val handles = handlesFor(logger.getClass)
+
+  def debug(text: String): Unit = handles.debug.invoke(logger, text)
+  def info(text: String): Unit = handles.info.invoke(logger, text)
+  def warn(text: String): Unit = handles.warn.invoke(logger, text)
+  def error(text: String): Unit = handles.error.invoke(logger, text)
+}
+
+private object ReflectiveLogger {
+  private final case class LoggerHandles(debug: MethodHandle, info: MethodHandle, warn: MethodHandle,
+      error: MethodHandle)
+
+  private val lookup = MethodHandles.publicLookup()
+  private val loggerMethodType = JMethodType.methodType(Void.TYPE, classOf[String])
+  private val handlesByLoggerClass = TrieMap.empty[Class[?], LoggerHandles]
 
-  def debug(text: String): Unit = debugMethod.invoke(logger, text)
-  def info(text: String): Unit = infoMethod.invoke(logger, text)
-  def warn(text: String): Unit = warnMethod.invoke(logger, text)
-  def error(text: String): Unit = errorMethod.invoke(logger, text)
+  private def handlesFor(loggerClass: Class[?]): LoggerHandles =
+    handlesByLoggerClass.getOrElseUpdate(
+      loggerClass,
+      LoggerHandles(
+        lookup.findVirtual(loggerClass, "debug", loggerMethodType),
+        lookup.findVirtual(loggerClass, "info", loggerMethodType),
+        lookup.findVirtual(loggerClass, "warn", loggerMethodType),
+        lookup.findVirtual(loggerClass, "error", loggerMethodType)))
 }
diff --git a/codegen/src/main/scala/org/apache/pekko/grpc/gen/Main.scala b/codegen/src/main/scala/org/apache/pekko/grpc/gen/Main.scala
index e3478c41..4f0e1d30 100644
--- a/codegen/src/main/scala/org/apache/pekko/grpc/gen/Main.scala
+++ b/codegen/src/main/scala/org/apache/pekko/grpc/gen/Main.scala
@@ -14,9 +14,12 @@
 package org.apache.pekko.grpc.gen
 
 import java.io.ByteArrayOutputStream
+import java.lang.invoke.{ MethodHandle, MethodHandles, MethodType => JMethodType }
 import java.net.URLDecoder
 import java.nio.charset.StandardCharsets
 
+import scala.collection.concurrent.TrieMap
+
 import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest
 import org.apache.pekko
 import pekko.grpc.gen.javadsl.{ JavaClientCodeGenerator, JavaInterfaceCodeGenerator, JavaServerCodeGenerator }
@@ -24,6 +27,10 @@ import pekko.grpc.gen.scaladsl.{ ScalaClientCodeGenerator, ScalaServerCodeGenera
 
 // This is the protoc plugin that the gradle plugin uses
 object Main {
+  private val publicLookup = MethodHandles.publicLookup()
+  private val generatorConstructorType = JMethodType.methodType(Void.TYPE)
+  private val generatorConstructors = TrieMap.empty[String, MethodHandle]
+
   def main(args: Array[String]): Unit = {
     val inBytes: Array[Byte] = {
       val baos = new ByteArrayOutputStream(math.max(64, System.in.available()))
@@ -84,7 +91,7 @@ object Main {
         else throw new IllegalArgumentException("At least one of generateClient or generateServer must be enabled")
       }
     val loadedExtraGenerators =
-      extraGenerators.map(cls => Class.forName(cls).getDeclaredConstructor().newInstance().asInstanceOf[CodeGenerator])
+      extraGenerators.map(loadExtraGenerator)
 
     (codeGenerators ++ loadedExtraGenerators).foreach { g =>
       val gout = g.run(req, logger)
@@ -92,4 +99,13 @@ object Main {
       System.out.flush()
     }
   }
+
+  private def loadExtraGenerator(className: String): CodeGenerator =
+    generatorConstructors.getOrElseUpdate(className, constructorHandle(className)).invoke()
+      .asInstanceOf[CodeGenerator]
+
+  private def constructorHandle(className: String): MethodHandle = {
+    val clazz = Class.forName(className)
+    publicLookup.findConstructor(clazz, generatorConstructorType)
+  }
 }
diff --git a/maven-plugin/src/main/scala/org/apache/pekko/grpc/maven/AbstractGenerateMojo.scala b/maven-plugin/src/main/scala/org/apache/pekko/grpc/maven/AbstractGenerateMojo.scala
index 11262253..7a62a2a8 100644
--- a/maven-plugin/src/main/scala/org/apache/pekko/grpc/maven/AbstractGenerateMojo.scala
+++ b/maven-plugin/src/main/scala/org/apache/pekko/grpc/maven/AbstractGenerateMojo.scala
@@ -14,8 +14,11 @@
 package org.apache.pekko.grpc.maven
 
 import java.io.{ ByteArrayOutputStream, File, PrintStream }
+import java.lang.invoke.{ MethodHandle, MethodHandles, MethodType }
 import java.nio.charset.StandardCharsets
 
+import scala.collection.concurrent.TrieMap
+
 import org.apache.pekko
 import pekko.grpc.gen.{ CodeGenerator, Logger, ProtocSettings }
 import pekko.grpc.gen.javadsl.{ JavaClientCodeGenerator, JavaInterfaceCodeGenerator, JavaServerCodeGenerator }
@@ -36,6 +39,9 @@ import scala.util.control.NoStackTrace
 object AbstractGenerateMojo {
   case class ProtocError(file: String, line: Int, pos: Int, message: String)
   private val ProtocErrorRegex = """(\w+\.\w+):(\d+):(\d+):\s(.*)""".r
+  private val publicLookup = MethodHandles.publicLookup()
+  private val generatorConstructorType = MethodType.methodType(Void.TYPE)
+  private val generatorConstructors = TrieMap.empty[String, MethodHandle]
 
   /** @return a left(parsed error) or a right(original error string) if it cannot be parsed */
   def parseError(errorLine: String): Either[ProtocError, String] =
@@ -46,6 +52,15 @@ object AbstractGenerateMojo {
         Right(unknown)
     }
 
+  private def loadExtraGenerator(className: String): CodeGenerator =
+    generatorConstructors.getOrElseUpdate(className, constructorHandle(className)).invoke()
+      .asInstanceOf[CodeGenerator]
+
+  private def constructorHandle(className: String): MethodHandle = {
+    val clazz = Class.forName(className)
+    publicLookup.findConstructor(clazz, generatorConstructorType)
+  }
+
   private def captureStdOutAndErr[T](block: => T): (String, String, T) = {
     val errBao = new ByteArrayOutputStream()
     val errPrinter = new PrintStream(errBao, true, StandardCharsets.UTF_8)
@@ -182,8 +197,7 @@ abstract class AbstractGenerateMojo @Inject() (buildContext: BuildContext) exten
     } else {
       import scala.jdk.CollectionConverters._
       val loadedExtraGenerators =
-        extraGenerators.asScala.map(cls =>
-          Class.forName(cls).getDeclaredConstructor().newInstance().asInstanceOf[CodeGenerator])
+        extraGenerators.asScala.map(AbstractGenerateMojo.loadExtraGenerator)
 
       val targets = language match {
         case Java =>
diff --git a/project/ReflectiveCodeGen.scala b/project/ReflectiveCodeGen.scala
index 80636113..fb377499 100644
--- a/project/ReflectiveCodeGen.scala
+++ b/project/ReflectiveCodeGen.scala
@@ -19,7 +19,6 @@ import ProtocPlugin.autoImport.PB
 import protocbridge.Target
 import sbt.ProjectRef
 import sbt.file
-import sbt.internal.inc.classpath.ClasspathUtil
 
 import scala.collection.mutable.ListBuffer
 import protocbridge.{ Artifact => BridgeArtifact }
@@ -80,7 +79,7 @@ object ReflectiveCodeGen extends AutoPlugin {
           }
         }.value,
         setCodeGenerator := Def.taskDyn {
-          // Scala 2.12 still uses the sbt-plugin/toolbox path. Library cross builds use codegen
+          // Scala 2.12 still uses the sbt-plugin classpath. Library cross builds use codegen
           // directly so forced 2.13/3.3 matrix runs do not compile the sbt 2 plugin project.
           val generatorProject =
             if (scalaBinaryVersion.value == "2.12") "sbt-plugin"
@@ -122,59 +121,15 @@ object ReflectiveCodeGen extends AutoPlugin {
       generatorSettings: Seq[String],
       targets: ListBuffer[Target],
       scalaBinaryVersion: String): Unit = {
-    if (scalaBinaryVersion != "2.12") {
-      loadAndSetGeneratorWithMethodHandles(
-        classpath,
-        languages0,
-        sources0,
-        extraGenerators0,
-        targetPath,
-        generatorSettings,
-        targets,
-        scalaBinaryVersion)
-      return
-    }
-
-    val languages = languages0.mkString(", ")
-    val sources = sources0.mkString(", ")
-    val extraGenerators = extraGenerators0.mkString(", ")
-    val generatorSettings1 = generatorSettings.mkString("\"", "\", \"", "\"")
-
-    val cp = classpath.map(_.data)
-    // ensure to set right parent classloader, so that protocbridge.ProtocCodeGenerator etc are
-    // compatible with what is already accessible from this sbt build
-    val loader = ClasspathUtil.toLoader(cp, classOf[protocbridge.ProtocCodeGenerator].getClassLoader)
-    import scala.reflect.runtime.universe
-    import scala.tools.reflect.ToolBox
-
-    val tb = universe.runtimeMirror(loader).mkToolBox()
-    val source =
-      s"""import org.apache.pekko.grpc.sbt.PekkoGrpcPlugin
-          |import org.apache.pekko.grpc.sbt.GeneratorBridge
-          |import PekkoGrpcPlugin.autoImport._
-          |import PekkoGrpc._
-          |import org.apache.pekko.grpc.gen.scaladsl._
-          |import org.apache.pekko.grpc.gen.javadsl._
-          |import org.apache.pekko.grpc.gen.CodeGenerator.ScalaBinaryVersion
-          |
-          |val languages: Seq[PekkoGrpc.Language] = Seq($languages)
-          |val sources: Seq[PekkoGrpc.GeneratedSource] = Seq($sources)
-          |val scalaBinaryVersion = ScalaBinaryVersion("$scalaBinaryVersion")
-          |
-          |val logger = org.apache.pekko.grpc.gen.StdoutLogger
-          |
-          |(targetPath: java.io.File, settings: Seq[String]) => {
-          |  val generators =
-          |    PekkoGrpcPlugin.generatorsFor(sources, languages, scalaBinaryVersion, logger) ++
-          |    Seq($extraGenerators).map(gen => GeneratorBridge.sandboxedGenerator(gen, scalaBinaryVersion, org.apache.pekko.grpc.gen.StdoutLogger))
-          |  PekkoGrpcPlugin.targetsFor(targetPath, settings, generators)
-          |}
-        """.stripMargin
-    val generatorsF = tb.eval(tb.parse(source)).asInstanceOf[(File, Seq[String]) => Seq[Target]]
-    val generators = generatorsF(targetPath, generatorSettings)
-
-    targets.clear()
-    targets ++= generators.asInstanceOf[Seq[Target]]
+    loadAndSetGeneratorWithMethodHandles(
+      classpath,
+      languages0,
+      sources0,
+      extraGenerators0,
+      targetPath,
+      generatorSettings,
+      targets,
+      scalaBinaryVersion)
   }
 
   private def loadAndSetGeneratorWithMethodHandles(
@@ -296,19 +251,19 @@ object ReflectiveCodeGen extends AutoPlugin {
       new ChildFirstClassLoader(classpath.map(_.toURI.toURL).toArray, classLoader)
     private val lookup = MethodHandles.publicLookup()
     private val moduleClass = childFirstClassLoader.loadClass(className)
-    private val module =
-      lookup.findStaticGetter(moduleClass, "MODULE$", moduleClass).invokeWithArguments()
+    private val module: Object =
+      lookup.findStaticGetter(moduleClass, "MODULE$", moduleClass).invoke()
     private val loggerClass = childFirstClassLoader.loadClass("org.apache.pekko.grpc.gen.Logger")
     private val loggerModuleClass = childFirstClassLoader.loadClass("org.apache.pekko.grpc.gen.SilencedLogger$")
-    private val logger =
-      lookup.findStaticGetter(loggerModuleClass, "MODULE$", loggerModuleClass).invokeWithArguments()
+    private val logger: Object =
+      lookup.findStaticGetter(loggerModuleClass, "MODULE$", loggerModuleClass).invoke()
     private val runMethod = lookup.findVirtual(
       moduleClass,
       "run",
       MethodType.methodType(classOf[Array[Byte]], classOf[Array[Byte]], loggerClass))
 
     override def run(request: Array[Byte]): Array[Byte] =
-      runMethod.invokeWithArguments(module, request.asInstanceOf[Object], logger).asInstanceOf[Array[Byte]]
+      runMethod.invoke(module, request.asInstanceOf[Object], logger).asInstanceOf[Array[Byte]]
 
     override def toString = s"MethodHandleProtocCodeGenerator($className)"
   }
diff --git a/sbt-plugin/src/main/scala/org/apache/pekko/grpc/sbt/GeneratorBridge.scala b/sbt-plugin/src/main/scala/org/apache/pekko/grpc/sbt/GeneratorBridge.scala
index 7334a285..1503fd4b 100644
--- a/sbt-plugin/src/main/scala/org/apache/pekko/grpc/sbt/GeneratorBridge.scala
+++ b/sbt-plugin/src/main/scala/org/apache/pekko/grpc/sbt/GeneratorBridge.scala
@@ -13,6 +13,8 @@
 
 package org.apache.pekko.grpc.sbt
 
+import java.lang.invoke.{ MethodHandles, MethodType }
+
 import org.apache.pekko
 import pekko.grpc.gen.Logger
 import pekko.grpc.gen.BuildInfo
@@ -58,22 +60,26 @@ object GeneratorBridge {
    */
   private class SandboxedProtocBridgeSbtPluginCodeGenerator(classLoader: ClassLoader, className: String, logger: Logger)
       extends protocbridge.ProtocCodeGenerator {
-    val genClass = classLoader.loadClass(className)
-    val module = genClass.getField("MODULE$").get(null)
+    private val lookup = MethodHandles.publicLookup()
+    private val genClass = classLoader.loadClass(className)
+    private val module: Object = lookup.findStaticGetter(genClass, "MODULE$", genClass).invoke()
+    private val loggerClass = classLoader.loadClass("org.apache.pekko.grpc.gen.Logger")
+    private val reflectiveLoggerClass = classLoader.loadClass("org.apache.pekko.grpc.gen.ReflectiveLogger")
     private val reflectiveLogger: Object =
-      classLoader
-        .loadClass("org.apache.pekko.grpc.gen.ReflectiveLogger")
-        .asInstanceOf[Class[Object]]
-        .getConstructor(classOf[Object])
-        .newInstance(logger)
+      lookup
+        .findConstructor(
+          reflectiveLoggerClass,
+          MethodType.methodType(Void.TYPE, classOf[Object]))
+        .invoke(logger)
 
-    private val runMethods =
-      module.getClass.getMethods
-        .find(m => m.getName == "run" && m.getParameterTypes()(0) == classOf[Array[Byte]])
-        .getOrElse(throw new RuntimeException("Could not find 'run' method that takes an Array[Byte]"))
+    private val runMethod =
+      lookup.findVirtual(
+        genClass,
+        "run",
+        MethodType.methodType(classOf[Array[Byte]], classOf[Array[Byte]], loggerClass))
 
     override def run(request: Array[Byte]): Array[Byte] =
-      runMethods.invoke(module, request, reflectiveLogger).asInstanceOf[Array[Byte]]
+      runMethod.invoke(module, request, reflectiveLogger).asInstanceOf[Array[Byte]]
     override def toString = s"SandboxedProtocBridgeSbtPluginCodeGenerator(${className})"
   }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pekko.apache.org
For additional commands, e-mail: commits-help@pekko.apache.org