Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.pekko.util;

final class FailingInitializerConstructor {
private static final Object INITIALIZED = failInitialization();

FailingInitializerConstructor() {}

private static Object failInitialization() {
throw new IllegalStateException("constructor-initializer-bug");
}
}

final class StaticMethodExceptionFixture {
static Object failInBody() {
throw new IllegalArgumentException("static-method-user-bug");
}
}

final class FailingInitializerStaticMethod {
private static final Object INITIALIZED = failInitialization();

static Object getInstance() {
return new Object();
}

private static Object failInitialization() {
throw new IllegalStateException("static-method-initializer-bug");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

package org.apache.pekko

import java.lang.invoke.{ MethodHandles, MethodType }

import org.apache.pekko.actor._

import org.scalatest.matchers.should.Matchers
Expand All @@ -37,6 +39,6 @@ class PekkoExceptionSpec extends AnyWordSpec with Matchers {
}

def verify(clazz: java.lang.Class[?]): Unit = {
clazz.getConstructor(Array(classOf[String]): _*)
MethodHandles.publicLookup().findConstructor(clazz, MethodType.methodType(Void.TYPE, classOf[String]))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@ class TestClassWithStringConstructor(val name: String) extends TestSuperclass
class TestClassWithDefaultConstructor extends TestSuperclass {
override def name = "default"
}
class TestClassWithPrivateConstructor private () extends TestSuperclass {
override def name = "private"
}
object TestClassWithPrivateConstructor {
def direct(): TestClassWithPrivateConstructor = new TestClassWithPrivateConstructor()
}
class TestClassWithThrowingConstructor(@scala.annotation.nowarn("msg=never used") value: String)
extends TestSuperclass {
throw new IllegalArgumentException("user-bug")
override def name = "unreachable"
}
object TestDynamicAccessObject extends TestSuperclass {
override def name = "object"
}

class DynamicAccessSpec extends AnyWordSpec with Matchers with BeforeAndAfterAll {
val system = ActorSystem()
Expand Down Expand Up @@ -74,6 +88,29 @@ class DynamicAccessSpec extends AnyWordSpec with Matchers with BeforeAndAfterAll
dynamicAccess.classIsOnClasspath("org.apache.pekko.actor.Actor") should ===(true)
}

"reuse constructor handles without changing private constructor access" in {
val clazz = classOf[TestClassWithPrivateConstructor]
dynamicAccess.createInstanceFor[TestClassWithPrivateConstructor](clazz, Nil).get.name should ===("private")
dynamicAccess.createInstanceFor[TestClassWithPrivateConstructor](clazz, Nil).get.name should ===("private")
}

"preserve the target exception from a failing constructor" in {
val result = dynamicAccess.createInstanceFor[TestClassWithThrowingConstructor](
classOf[TestClassWithThrowingConstructor],
immutable.Seq(classOf[String] -> "ignored"))
val exception = result.failed.get
exception shouldBe a[IllegalArgumentException]
exception.getMessage should ===("user-bug")
}

"reuse the Scala object field handle" in {
val withoutSuffix =
dynamicAccess.getObjectFor[TestSuperclass]("org.apache.pekko.actor.TestDynamicAccessObject").get
val withSuffix = dynamicAccess.getObjectFor[TestSuperclass]("org.apache.pekko.actor.TestDynamicAccessObject$").get
(withoutSuffix should be).theSameInstanceAs(TestDynamicAccessObject)
(withSuffix should be).theSameInstanceAs(TestDynamicAccessObject)
}

def instantiateWithDefaultOrStringCtor(fqcn: String): Try[TestSuperclass] =
// recoverWith doesn't work with scala 2.13.0-M5
// https://github.com/scala/bug/issues/11242
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ class ByteIteratorSpec extends AnyWordSpec with Matchers {
freshIterator().indexOf(0x10, 3) should be(5)

// There is also an indexOf with another signature, which is hard to invoke :D
val otherIndexOfHandle =
java.lang.invoke.MethodHandles.publicLookup().findVirtual(
classOf[ByteIterator],
"indexOf",
java.lang.invoke.MethodType.methodType(classOf[Int], classOf[Byte], classOf[Int]))
def otherIndexOf(iterator: ByteIterator, byte: Byte, from: Int): Int =
classOf[ByteIterator]
.getMethod("indexOf", classOf[Byte], classOf[Int])
.invoke(iterator, byte.asInstanceOf[Object], from.asInstanceOf[Object])
.asInstanceOf[Int]
otherIndexOfHandle.invoke(iterator, byte, from).asInstanceOf[Int]

otherIndexOf(freshIterator(), 0x20, 1) should be(1)
otherIndexOf(freshIterator(), 0x10, 1) should be(2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ class ByteStringInitializationSpec extends AnyWordSpec with Matchers {
}
}

cleanCl
.loadClass("org.apache.pekko.util.ByteStringInitTest")
.getDeclaredConstructor()
.newInstance()
val clazz = cleanCl.loadClass("org.apache.pekko.util.ByteStringInitTest")
java.lang.invoke.MethodHandles
.privateLookupIn(clazz, java.lang.invoke.MethodHandles.lookup())
.findConstructor(clazz, java.lang.invoke.MethodType.methodType(Void.TYPE))
.invokeWithArguments()
.asInstanceOf[Runnable]
.run()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

package org.apache.pekko.util

import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationTargetException
import java.util.concurrent.TimeoutException

import scala.annotation.nowarn
import scala.collection.immutable

Expand All @@ -30,6 +34,15 @@ object ReflectSpec {
def this(a: A) = this(a, null)
def this(b: B) = this(null, b)
}

final class StringOnly(val value: String)
final class PrivateConstructor private (val value: String)
object PrivateConstructor {
def direct(value: String): PrivateConstructor = new PrivateConstructor(value)
}
final class ThrowingConstructor(@nowarn("msg=never used") value: String) {
throw new IllegalArgumentException("user-bug")
}
}

class ReflectSpec extends AnyWordSpec with Matchers {
Expand All @@ -50,7 +63,7 @@ class ReflectSpec extends AnyWordSpec with Matchers {
}
"deal with `null` in 1 matching case" in {
val constructor = Reflect.findConstructor(classOf[One], immutable.Seq(null))
constructor.newInstance(null)
constructor.invoke(null.asInstanceOf[AnyRef])
}
"deal with multiple constructors" in {
Reflect.findConstructor(classOf[MultipleOne], immutable.Seq(new A))
Expand All @@ -62,6 +75,60 @@ class ReflectSpec extends AnyWordSpec with Matchers {
Reflect.findConstructor(classOf[MultipleOne], immutable.Seq(null))
}
}
"use public lookup for public JDK constructors in non-open modules" in {
val constructor = Reflect.findConstructor(classOf[TimeoutException], immutable.Seq("err"))
val instance = Reflect.instantiate[TimeoutException](constructor, immutable.Seq("err"))
instance.getMessage should ===("err")
}
"not confuse null with an Object argument across lookups" in {
Reflect.instantiate(classOf[StringOnly], immutable.Seq(null)).value shouldBe null
intercept[IllegalArgumentException] {
Reflect.findConstructor(classOf[StringOnly], immutable.Seq(new Object))
}.getMessage should include("no matching constructor")
}
"access private constructors when the package is open" in {
Reflect.instantiate(classOf[PrivateConstructor], immutable.Seq("private")).value should ===("private")
}
"preserve InvocationTargetException for constructor failures" in {
val exception = intercept[InvocationTargetException] {
Reflect.instantiate(classOf[ThrowingConstructor], immutable.Seq("ignored"))
}
exception.getCause shouldBe a[IllegalArgumentException]
exception.getCause.getMessage should ===("user-bug")
}
"not wrap constructor class-initialization failures" in {
val exception = intercept[ExceptionInInitializerError] {
Reflect.instantiate(classOf[FailingInitializerConstructor])
}
exception.getCause.getMessage should ===("constructor-initializer-bug")
}
"invoke varargs constructors as fixed arity" in {
val processBuilder =
Reflect.instantiate(classOf[ProcessBuilder], immutable.Seq(Array("echo", "fixed-arity")))
processBuilder.command().toArray should ===(Array[AnyRef]("echo", "fixed-arity"))
}
}

"Reflect#invokeStaticNoArg" must {
"preserve InvocationTargetException for target failures" in {
val clazz = classOf[StaticMethodExceptionFixture]
val callerLookup = MethodHandles.lookup()
val method = Reflect.findStaticNoArgMethod(clazz, "failInBody", callerLookup)
val exception = intercept[InvocationTargetException] {
Reflect.invokeStaticNoArg[AnyRef](clazz, method, callerLookup)
}
exception.getCause.getMessage should ===("static-method-user-bug")
}

"not wrap static method class-initialization failures" in {
val clazz = classOf[FailingInitializerStaticMethod]
val callerLookup = MethodHandles.lookup()
val method = Reflect.findStaticNoArgMethod(clazz, "getInstance", callerLookup)
val exception = intercept[ExceptionInInitializerError] {
Reflect.invokeStaticNoArg[AnyRef](clazz, method, callerLookup)
}
exception.getCause.getMessage should ===("static-method-initializer-bug")
}
}

"Reflect#getCallerClass" must {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

package org.apache.pekko.actor.typed.internal

import java.lang.invoke.MethodHandles
import java.util.concurrent.{ ConcurrentHashMap, CountDownLatch }

import scala.annotation.tailrec
Expand All @@ -23,6 +24,7 @@ import org.apache.pekko
import pekko.actor.typed.{ ActorSystem, Extension, ExtensionId, Extensions }
import pekko.actor.typed.ExtensionSetup
import pekko.annotation.InternalApi
import pekko.util.Reflect

/**
* INTERNAL API
Expand All @@ -33,6 +35,7 @@ import pekko.annotation.InternalApi
private[pekko] trait ExtensionsImpl extends Extensions { self: ActorSystem[?] with InternalRecipientRef[?] =>

private val extensions = new ConcurrentHashMap[ExtensionId[?], AnyRef]
private val lookup = MethodHandles.lookup()

/**
* Hook for ActorSystem to load extensions on startup
Expand Down Expand Up @@ -70,8 +73,8 @@ private[pekko] trait ExtensionsImpl extends Extensions { self: ActorSystem[?] wi
dynamicAccess.getClassFor[ExtensionId[Extension]](extensionIdFQCN).flatMap[ExtensionId[Extension]] {
(clazz: Class[?]) =>
Try {
val singletonAccessor = clazz.getDeclaredMethod("getInstance")
singletonAccessor.invoke(null).asInstanceOf[ExtensionId[Extension]]
val handle = Reflect.findStaticNoArgMethod(clazz, "getInstance", lookup)
Reflect.invokeStaticNoArg[ExtensionId[Extension]](clazz, handle, lookup)
}
}

Expand Down
11 changes: 6 additions & 5 deletions actor/src/main/java/org/apache/pekko/io/ByteBufferCleaner.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.invoke.VarHandle;
import java.nio.ByteBuffer;

/**
Expand Down Expand Up @@ -50,11 +50,12 @@ private static final class Java9Cleaner implements Cleaner {
private final MethodHandle invokeCleaner;

private Java9Cleaner() throws ReflectiveOperationException {
final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
final Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
final Object theUnsafe = field.get(null);
MethodHandles.Lookup lookup = MethodHandles.lookup();
final Class<?> unsafeClass = lookup.findClass("sun.misc.Unsafe");
final VarHandle unsafeHandle =
MethodHandles.privateLookupIn(unsafeClass, lookup)
.findStaticVarHandle(unsafeClass, "theUnsafe", unsafeClass);
final Object theUnsafe = unsafeHandle.get();
MethodHandle invokeCleaner =
lookup.findVirtual(
unsafeClass, "invokeCleaner", methodType(void.class, ByteBuffer.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,25 @@ private[pekko] class TypedCreatorFunctionConsumer(clz: Class[? <: Actor], creato
*/
private[pekko] class ArgsReflectConstructor(clz: Class[? <: Actor], args: immutable.Seq[Any])
extends IndirectActorProducer {
private val constructor = Reflect.findConstructor(clz, args)
private val constructor = Reflect.constructorInvoker(Reflect.findConstructor(clz, args))
private val arguments = Reflect.argumentArray(args)
private lazy val initialized: Unit = Reflect.ensureInitialized(clz)
override def actorClass = clz
override def produce() = Reflect.instantiate(constructor, args)
override def produce() = {
initialized
Reflect.instantiate[Actor](constructor, arguments)
}
}

/**
* INTERNAL API
*/
private[pekko] class NoArgsReflectConstructor(clz: Class[? <: Actor]) extends IndirectActorProducer {
Reflect.findConstructor(clz, List.empty)
private val constructor = Reflect.noArgsConstructorInvoker(Reflect.findConstructor(clz, Nil))
private lazy val initialized: Unit = Reflect.ensureInitialized(clz)
override def actorClass = clz
override def produce() = Reflect.instantiate(clz)
override def produce() = {
initialized
Reflect.instantiate[Actor](constructor)
}
}
Loading