diff --git a/engine-tests/src/test/java/org/terasology/engine/reflection/TestInjectInvoke.java b/engine-tests/src/test/java/org/terasology/engine/reflection/TestInjectInvoke.java new file mode 100644 index 00000000000..3e9f3513273 --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/reflection/TestInjectInvoke.java @@ -0,0 +1,52 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.reflection; + +import org.junit.jupiter.api.Test; +import org.terasology.engine.context.internal.ContextImpl; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Properties; + +import static com.google.common.truth.Truth.assertThat; + +public class TestInjectInvoke { + Integer red(String s) { + return s.length(); + } + + URI green(File file) { + return file.toURI(); + } + + String yellow(Properties p, String s) { + return p.getProperty("hello") + s; + } + + InvokingContext makeContext() { + InvokingContext context = new InvokingContext(new ContextImpl()); + context.put(String.class, "12345678"); + context.put(File.class, new File("/dev/null")); + Properties p = new Properties(); + p.put("hello", "zero"); + context.put(Properties.class, p); + return context; + } + + @Test + void canSupplyArgumentFromContextUsingSerialization() throws URISyntaxException { + var context = makeContext(); + assertThat(context.invoke(this::red)).isEqualTo(8); + assertThat(context.invoke(this::green)).isEqualTo(new URI("file:/dev/null")); + } + + @Test + void canSupplyTwoArgumentsFromContextUsingSerialization() { + var context = makeContext(); + assertThat(context.invoke(this::yellow)).isEqualTo("zero12345678"); + } + +} diff --git a/engine-tests/src/test/java/org/terasology/engine/registry/CoreRegistryTest.java b/engine-tests/src/test/java/org/terasology/engine/registry/CoreRegistryTest.java index dad43023317..39f85c11bbe 100644 --- a/engine-tests/src/test/java/org/terasology/engine/registry/CoreRegistryTest.java +++ b/engine-tests/src/test/java/org/terasology/engine/registry/CoreRegistryTest.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.registry; @@ -82,7 +82,7 @@ private static class ContextImplementation implements Context { private final Map, Object> map = Maps.newConcurrentMap(); @Override - public T get(Class type) { + public T get(Class type) { T result = type.cast(map.get(type)); if (result != null) { return result; diff --git a/engine/src/main/java/org/terasology/engine/context/Context.java b/engine/src/main/java/org/terasology/engine/context/Context.java index 2384ebd6c9c..2aded73962e 100644 --- a/engine/src/main/java/org/terasology/engine/context/Context.java +++ b/engine/src/main/java/org/terasology/engine/context/Context.java @@ -1,9 +1,11 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.context; import org.terasology.gestalt.module.sandbox.API; +import java.util.NoSuchElementException; + /** * Provides classes with the utility objects that belong to the context they are running in. * @@ -23,7 +25,15 @@ public interface Context { /** * @return the object that is known in this context for this type. */ - T get(Class type); + T get(Class type); + + default T getValue(Class type) { + T value = get(type); + if (value == null) { + throw new NoSuchElementException(type.toString()); + } + return value; + } /** * Makes the object known in this context to be the object to work with for the given type. diff --git a/engine/src/main/java/org/terasology/engine/context/internal/ContextImpl.java b/engine/src/main/java/org/terasology/engine/context/internal/ContextImpl.java index 28e94772338..33c13c977d7 100644 --- a/engine/src/main/java/org/terasology/engine/context/internal/ContextImpl.java +++ b/engine/src/main/java/org/terasology/engine/context/internal/ContextImpl.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.context.internal; @@ -13,7 +13,7 @@ public class ContextImpl implements Context { private final Context parent; - private final Map, Object> map = Maps.newConcurrentMap(); + private final Map, Object> map = Maps.newConcurrentMap(); /** @@ -30,7 +30,7 @@ public ContextImpl() { } @Override - public T get(Class type) { + public T get(Class type) { if (type == Context.class) { return type.cast(this); } @@ -41,7 +41,7 @@ public T get(Class type) { if (parent != null) { return parent.get(type); } - return result; + return null; } @Override diff --git a/engine/src/main/java/org/terasology/engine/context/internal/MockContext.java b/engine/src/main/java/org/terasology/engine/context/internal/MockContext.java index f5b5c7ed614..90e77ab705f 100644 --- a/engine/src/main/java/org/terasology/engine/context/internal/MockContext.java +++ b/engine/src/main/java/org/terasology/engine/context/internal/MockContext.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.context.internal; @@ -6,7 +6,7 @@ public class MockContext implements Context { @Override - public T get(Class type) { + public T get(Class type) { return null; } diff --git a/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java b/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java new file mode 100644 index 00000000000..657f239c666 --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java @@ -0,0 +1,83 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.reflection; + +import org.terasology.engine.context.Context; + +import java.lang.invoke.SerializedLambda; +import java.util.List; + +import static com.google.common.base.Verify.verify; + +@SuppressWarnings("checkstyle:MethodTypeParameterName") +public class InvokingContext implements Context { + + private final Context inner; + + public InvokingContext(Context inner) { + this.inner = inner; + } + + public R invoke(InvokingHelpers.SerializableFunction func) { + SerializedLambda serializedLambda = InvokingHelpers.getSerializedLambdaUnchecked(func); + + List> params = InvokingHelpers.getLambdaParameters(serializedLambda); + verify(params.size() == 1, "Expected exactly one parameter, found %s", params); + + @SuppressWarnings("unchecked") Class clazz = (Class) params.get(0); + return func.apply(inner.getValue(clazz)); + } + + public R invoke(InvokingHelpers.SerializableBiFunction func) { + // For small numbers of args, we could write out `f.apply(x1, …, xN)` by hand. + // But to generalize, we can use a MethodHandle. + return InvokingHelpers.invokeProvidingParametersByType( + InvokingHelpers.getSerializedLambdaUnchecked(func), + inner::getValue); + } + + public R invoke(InvokingHelpers.SerializableFunction3 func) { + return InvokingHelpers.invokeProvidingParametersByType( + InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue); + } + + public R invoke(InvokingHelpers.SerializableFunction4 func) { + return InvokingHelpers.invokeProvidingParametersByType( + InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue); + } + + public R invoke(InvokingHelpers.SerializableFunction5 func) { + return InvokingHelpers.invokeProvidingParametersByType( + InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue); + } + + public R invoke(InvokingHelpers.SerializableFunction6 func) { + return InvokingHelpers.invokeProvidingParametersByType( + InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue); + } + + public R invoke( + InvokingHelpers.SerializableFunction7 func) { + return InvokingHelpers.invokeProvidingParametersByType( + InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue); + } + + public R invoke( + InvokingHelpers.SerializableFunction8 func) { + return InvokingHelpers.invokeProvidingParametersByType( + InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue); + } + + /* *** Delegate to wrapped Context *** */ + + @Override + public T get(Class type) { + return inner.get(type); + } + + @Override + public void put(Class type, U object) { + inner.put(type, object); + } +} diff --git a/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java b/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java new file mode 100644 index 00000000000..6f4b83ba5ff --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java @@ -0,0 +1,125 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.reflection; + +import com.google.common.base.Throwables; + +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +public final class InvokingHelpers { + private InvokingHelpers() { } + + /** + * This relies on {@link Method#setAccessible} on a private final field. + *

+ * Is that bad? You might think so, but {@link Serializable} documentation says that + * Serializable objects may have that method even though it is not defined in + * the interface. So it's probably okay? + *

+ * Alternatives? + *

+ * Are there APIs to get the replaced-for-serialization object without trying to do + * do {@link Method#setAccessible} on arbitrary objects? + *

+ * We could do {@link java.io.ObjectStreamClass#lookup(Class) ObjectStreamClass.lookup}, + * that's the model that {@link ObjectOutputStream} uses. It has useful methods like + * {@code hasWriteReplaceMethod} and {@code invokeWriteReplace}. Unfortunately, those + * methods aren't public, so they're no help if we're trying to avoid hacking around + * visibility restrictions. + *

+ * It is also possible to run the object through an {@link ObjectOutputStream} and + * capture the result. + * That's worth trying if this breaks, but otherwise it's a lot of extra hassle. + */ + public static SerializedLambda getSerializedLambda(Serializable obj) throws IllegalAccessException { + Method writeReplace; + try { + writeReplace = obj.getClass().getDeclaredMethod("writeReplace"); + Object replacement = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + writeReplace.setAccessible(true); + return writeReplace.invoke(obj); + }); + return (SerializedLambda) replacement; + } catch (PrivilegedActionException | NoSuchMethodException e) { + Throwables.throwIfUnchecked(e.getCause()); + Throwables.throwIfInstanceOf(e.getCause(), IllegalAccessException.class); + throw new RuntimeException(e); + } + } + + public static SerializedLambda getSerializedLambdaUnchecked(Serializable obj) { + try { + return getSerializedLambda(obj); + } catch (IllegalAccessException e) { + Throwables.throwIfUnchecked(e); + throw new RuntimeException(e); + } + } + + public static List> getLambdaParameters(SerializedLambda serializedLambda) { + var methodType = MethodType.fromMethodDescriptorString( + serializedLambda.getImplMethodSignature(), + serializedLambda.getClass().getClassLoader() + ); + return methodType.parameterList(); + } + + public static R invokeProvidingParametersByType(SerializedLambda serializedLambda, Function, ?> provider) { + // For small numbers of args, we could write out `f.apply(x1, …, xN)` by hand. + // But to generalize, we can use a MethodHandle. + MethodHandle mh; + try { + mh = MethodHandleAdapters.ofLambda(serializedLambda); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + + var args = Arrays.stream(mh.type().parameterArray()) + .map(provider) + .collect(Collectors.toUnmodifiableList()); + + try { + @SuppressWarnings("unchecked") R result = (R) mh.invokeWithArguments(args); + return result; + } catch (Throwable e) { + Throwables.throwIfUnchecked(e); + throw new RuntimeException(e); + } + } + + interface SerializableFunction extends Function, Serializable { } + + interface SerializableBiFunction extends BiFunction, Serializable { } + + interface SerializableFunction3 extends Serializable, + reactor.function.Function3 { } + + interface SerializableFunction4 extends Serializable, + reactor.function.Function4 { } + + interface SerializableFunction5 extends Serializable, + reactor.function.Function5 { } + + interface SerializableFunction6 extends Serializable, + reactor.function.Function6 { } + + interface SerializableFunction7 extends Serializable, + reactor.function.Function7 { } + + interface SerializableFunction8 extends Serializable, + reactor.function.Function8 { } +} diff --git a/engine/src/main/java/org/terasology/engine/reflection/MethodHandleAdapters.java b/engine/src/main/java/org/terasology/engine/reflection/MethodHandleAdapters.java new file mode 100644 index 00000000000..4505a7f1d84 --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/reflection/MethodHandleAdapters.java @@ -0,0 +1,82 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.reflection; + +import com.google.common.base.Throwables; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandleInfo; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.invoke.SerializedLambda; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +public final class MethodHandleAdapters { + private MethodHandleAdapters() { } + + static MethodHandle ofLambda(SerializedLambda serializedLambda) throws ReflectiveOperationException { + // SerializedLambda's implementation has a reference to the capturing class, + // but it only exposes its name, so we'll have to look it up again. + String capturingClassName = serializedLambda.getCapturingClass().replace('/', '.'); + + MethodHandles.Lookup lookup; + + try { + // I'm guessing about the AccessController-related stuff, but the intention + // is that we get a Lookup instance that matches the capturing site of the lambda. + lookup = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + MethodHandles.Lookup ourLookup = MethodHandles.lookup(); + Class capturingClass = ourLookup.findClass(capturingClassName); + return MethodHandles.privateLookupIn(capturingClass, ourLookup); + }); + } catch (PrivilegedActionException e) { + Throwables.throwIfUnchecked(e.getCause()); + Throwables.throwIfInstanceOf(e.getCause(), ReflectiveOperationException.class); + throw new RuntimeException(e); + } + + Class implClass = lookup.findClass(serializedLambda.getImplClass().replace('/', '.')); + String name = serializedLambda.getImplMethodName(); + + // It seems weird to be carefully using the Lookup interface to find classes, + // and then have the MethodType back to using a ClassLoader. But I guess we + // trust `lookup.find*` to not return anything it shouldn't in the end. + MethodType methodType = MethodType.fromMethodDescriptorString( + serializedLambda.getImplMethodSignature(), + lookup.lookupClass().getClassLoader() + ); + + Object receiver = null; + if (serializedLambda.getCapturedArgCount() > 0) { + receiver = serializedLambda.getCapturedArg(0); + } + + // Surely this code must exist somewhere else already. + switch (serializedLambda.getImplMethodKind()) { + case MethodHandleInfo.REF_getField: + return lookup.findGetter(implClass, name, methodType.returnType()).bindTo(receiver); + case MethodHandleInfo.REF_getStatic: + return lookup.findStaticGetter(implClass, name, methodType.returnType()); + case MethodHandleInfo.REF_putField: + return lookup.findSetter(implClass, name, methodType.parameterType(0)).bindTo(receiver); + case MethodHandleInfo.REF_putStatic: + return lookup.findStaticSetter(implClass, name, methodType.parameterType(0)); + case MethodHandleInfo.REF_invokeInterface: + case MethodHandleInfo.REF_invokeVirtual: + return lookup.findVirtual(implClass, name, methodType).bindTo(receiver); + case MethodHandleInfo.REF_invokeStatic: + return lookup.findStatic(implClass, name, methodType); + case MethodHandleInfo.REF_invokeSpecial: + return lookup.findSpecial(implClass, name, methodType, lookup.lookupClass()) + .bindTo(receiver); + case MethodHandleInfo.REF_newInvokeSpecial: + return lookup.findConstructor(implClass, methodType); + default: + throw new RuntimeException("Not implemented for " + + MethodHandleInfo.referenceKindToString(serializedLambda.getImplMethodKind())); + } + } +}