From 0acbecdfa7dab3b29fb9cb6d7fb52dabf2296ad7 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Wed, 18 May 2022 03:28:10 -0700 Subject: [PATCH 1/3] WIP: InvokingContext injects ::method references with values from the Context --- .../engine/reflection/TestInjectInvoke.java | 81 ++++++++++++++++++ .../engine/registry/CoreRegistryTest.java | 4 +- .../terasology/engine/context/Context.java | 14 +++- .../engine/context/internal/ContextImpl.java | 8 +- .../engine/context/internal/MockContext.java | 4 +- .../reflection/CaptureSerializedOutput.java | 65 +++++++++++++++ .../reflection/ExtractSerializedLambda.java | 12 +++ .../engine/reflection/InvokeWriteReplace.java | 68 +++++++++++++++ .../engine/reflection/InvokingContext.java | 79 ++++++++++++++++++ .../engine/reflection/InvokingHelpers.java | 39 +++++++++ .../reflection/MethodHandleAdapters.java | 82 +++++++++++++++++++ .../engine/reflection/TypedFunction.java | 41 ++++++++++ 12 files changed, 487 insertions(+), 10 deletions(-) create mode 100644 engine-tests/src/test/java/org/terasology/engine/reflection/TestInjectInvoke.java create mode 100644 engine/src/main/java/org/terasology/engine/reflection/CaptureSerializedOutput.java create mode 100644 engine/src/main/java/org/terasology/engine/reflection/ExtractSerializedLambda.java create mode 100644 engine/src/main/java/org/terasology/engine/reflection/InvokeWriteReplace.java create mode 100644 engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java create mode 100644 engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java create mode 100644 engine/src/main/java/org/terasology/engine/reflection/MethodHandleAdapters.java create mode 100644 engine/src/main/java/org/terasology/engine/reflection/TypedFunction.java 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..fdd9fa0094e --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/reflection/TestInjectInvoke.java @@ -0,0 +1,81 @@ +// 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; +import static org.terasology.engine.reflection.InvokingHelpers.argType; + +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 canGetTypeOfFunctionArgument() { + assertThat(argType(this::red)).isAssignableTo(String.class); + assertThat(argType(this::green)).isAssignableTo(File.class); + } + + @Test + void canGetTypeOfFunctionArgumentThenDoStuffIfUsingReturnedFunction() throws URISyntaxException { + var r = TypedFunction.of(this::green); + URI greenResult = r.apply(new File("/dev/null")); + assertThat(greenResult).isEqualTo(new URI("file:/dev/null")); + } + + @Test + void canSupplyArgumentFromContext() throws URISyntaxException { + var context = makeContext(); + + var greenResult = context.invoke(this::green); + assertThat(greenResult).isEqualTo(new URI("file:/dev/null")); + assertThat(greenResult.getScheme()).isEqualTo("file"); + + // TODO: That worked but this fails. JVM bug? + // --debug=verboseResolution=all looks okay on the compiler side, must be runtime. + // assertThat(context.invoke(this::green)).isEqualTo(new URI("file:/dev/null")); + + assertThat(context.invoke(this::red)).isEqualTo(8); + } + + @Test + void canSupplyArgumentFromContextUsingSerialization() throws URISyntaxException { + var context = makeContext(); + assertThat(context.invokeS(this::red)).isEqualTo(8); + assertThat(context.invokeS(this::green)).isEqualTo(new URI("file:/dev/null")); + } + + @Test + void canSupplyTwoArgumentsFromContextUsingSerialization() { + var context = makeContext(); + assertThat(context.invokeS(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/CaptureSerializedOutput.java b/engine/src/main/java/org/terasology/engine/reflection/CaptureSerializedOutput.java new file mode 100644 index 00000000000..1a0eaafbe2f --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/reflection/CaptureSerializedOutput.java @@ -0,0 +1,65 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.reflection; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.io.UncheckedIOException; +import java.lang.invoke.SerializedLambda; +import java.util.ArrayList; +import java.util.List; + +/** + * This technique, obtuse as it is, does not require overriding anything marked private. + *

+ * Does require {@link java.io.SerializablePermission SerializablePermission("enableSubstitution")}, + * as per {@link ObjectOutputStream#enableReplaceObject}. + */ +@SuppressWarnings("JavadocReference") +public class CaptureSerializedOutput implements ExtractSerializedLambda { + static SerializedLambda getSerializedLambdaFromOutputStream(Serializable obj) { + List outputObjects; + try (var output = new SpyingOutputObjectStream(new ByteArrayOutputStream())) { + try { + output.writeObject(obj); + } finally { + outputObjects = output.getEmittedObjects(); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return (SerializedLambda) outputObjects.get(0); + } + + @Override + public SerializedLambda of(Serializable lambda) { + return getSerializedLambdaFromOutputStream(lambda); + } + + static class SpyingOutputObjectStream extends ObjectOutputStream { + private final List emittedObjects = new ArrayList<>(); + + SpyingOutputObjectStream(OutputStream out) throws IOException { + super(out); + enableReplaceObject(true); + } + + @Override + protected Object replaceObject(Object obj) { + emittedObjects.add(obj); + // The user of this class only cares about the rewritten form of the object, + // not the bytestream. Returning null saves the serializer work and avoids + // potential serialization errors. + return null; + } + + public List getEmittedObjects() { + return List.copyOf(emittedObjects); + } + } +} diff --git a/engine/src/main/java/org/terasology/engine/reflection/ExtractSerializedLambda.java b/engine/src/main/java/org/terasology/engine/reflection/ExtractSerializedLambda.java new file mode 100644 index 00000000000..01c987dda18 --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/reflection/ExtractSerializedLambda.java @@ -0,0 +1,12 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.reflection; + +import java.io.Serializable; +import java.lang.invoke.SerializedLambda; + +@FunctionalInterface +public interface ExtractSerializedLambda { + SerializedLambda of(Serializable lambda); +} diff --git a/engine/src/main/java/org/terasology/engine/reflection/InvokeWriteReplace.java b/engine/src/main/java/org/terasology/engine/reflection/InvokeWriteReplace.java new file mode 100644 index 00000000000..b593998a88d --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/reflection/InvokeWriteReplace.java @@ -0,0 +1,68 @@ +// 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.SerializedLambda; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; + +/** + * 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. + */ +public class InvokeWriteReplace implements ExtractSerializedLambda { + static SerializedLambda getSerializedLambda(Serializable obj) throws IllegalAccessException { + Method writeReplace; + try { + writeReplace = obj.getClass().getDeclaredMethod("writeReplace"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + try { + Object replacement = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + writeReplace.setAccessible(true); + return writeReplace.invoke(obj); + }); + return (SerializedLambda) replacement; + } catch (PrivilegedActionException e) { + Throwables.throwIfUnchecked(e.getCause()); + Throwables.throwIfInstanceOf(e.getCause(), IllegalAccessException.class); + throw new RuntimeException(e); + } + } + + static SerializedLambda getSerializedLambdaUnchecked(Serializable obj) { + try { + return getSerializedLambda(obj); + } catch (IllegalAccessException e) { + Throwables.throwIfUnchecked(e); + throw new RuntimeException(e); + } + } + + @Override + public SerializedLambda of(Serializable lambda) { + 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..ea0c9418b43 --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java @@ -0,0 +1,79 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.reflection; + +import com.google.common.base.Throwables; +import org.terasology.engine.context.Context; +import reactor.core.publisher.Flux; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.SerializedLambda; +import java.util.List; +import java.util.function.Function; + +import static com.google.common.base.Verify.verify; + +public class InvokingContext implements Context { + + private final Context inner; + + public InvokingContext(Context inner) { + this.inner = inner; + } + + @SafeVarargs // SafeVarargs requires `final` + final R invoke(Function func, T... ts) { + var clazz = InvokingHelpers.argType(func, ts); + return func.apply(inner.getValue(clazz)); + } + + R invokeS(InvokingHelpers.SerializableFunction func) { + SerializedLambda serializedLambda = InvokeWriteReplace.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)); + } + + R invokeS(InvokingHelpers.SerializableBiFunction func) { + return invokeSerializedLambda(InvokeWriteReplace.getSerializedLambdaUnchecked(func)); + } + + R invokeSerializedLambda(SerializedLambda serializedLambda) { + // 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 = Flux.fromArray(mh.type().parameterArray()) + .map(inner::getValue) + .collectList().block(); + + try { + @SuppressWarnings("unchecked") R result = (R) mh.invokeWithArguments(args); + return result; + } catch (Throwable e) { + Throwables.throwIfUnchecked(e); + throw new RuntimeException(e); + } + } + + /* *** 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..3bd0b1d9036 --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java @@ -0,0 +1,39 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.reflection; + +import java.io.Serializable; +import java.lang.invoke.MethodType; +import java.lang.invoke.SerializedLambda; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +public final class InvokingHelpers { + private InvokingHelpers() { } + + /** + * The Class of the function's first argument. + *

+ * The trick here is that even if you pass no {@code ts}, the method still receives a zero-length + * array object, and we can read the class of that. + */ + @SafeVarargs + static Class argType(Function func, T... ts) { + return TypedFunction.of(func, ts).getInputClass(); + } + + static List> getLambdaParameters(SerializedLambda serializedLambda) { + var methodType = MethodType.fromMethodDescriptorString( + serializedLambda.getImplMethodSignature(), + serializedLambda.getClass().getClassLoader() + ); + return methodType.parameterList(); + } + + interface SerializableFunction extends Function, Serializable { } + + interface SerializableBiFunction extends BiFunction, Serializable { } + +} 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())); + } + } +} diff --git a/engine/src/main/java/org/terasology/engine/reflection/TypedFunction.java b/engine/src/main/java/org/terasology/engine/reflection/TypedFunction.java new file mode 100644 index 00000000000..61034368eb3 --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/reflection/TypedFunction.java @@ -0,0 +1,41 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.reflection; + +import java.util.function.Function; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class TypedFunction implements Function { + private final Class clazz; + private final Function func; + + TypedFunction(Class clazz, Function func) { + this.clazz = clazz; + this.func = func; + } + + @SafeVarargs + static TypedFunction of(Function func, T... ts) { + checkNotNull((Object) ts, "did not received typed varargs array"); + checkArgument(ts.length == 0, "Args to ts are ignored, do not supply them."); + @SuppressWarnings("unchecked") Class clazz = (Class) ts.getClass().getComponentType(); + if (clazz.equals(Object.class)) { + //noinspection ImplicitArrayToString + throw new RuntimeException(String.format( + "Looks like Object, which is more generic than you want. %s of %s", clazz, ts)); + } + return new TypedFunction<>(clazz, func); + } + + @Override + public R apply(T t) { + return func.apply(t); + } + + public Class getInputClass() { + return clazz; + } +} From addc872a0a11063c4fa43f2b8828dda1ebf614d3 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Wed, 18 May 2022 12:25:17 -0700 Subject: [PATCH 2/3] WIP: stick with only the SerializedLambda.writeReplace technique It seems to be the most predictable and versatile. Removed the alternate approaches to slim down the code. --- .../engine/reflection/TestInjectInvoke.java | 35 +------- .../reflection/CaptureSerializedOutput.java | 65 -------------- .../reflection/ExtractSerializedLambda.java | 12 --- .../engine/reflection/InvokeWriteReplace.java | 68 --------------- .../engine/reflection/InvokingContext.java | 41 ++------- .../engine/reflection/InvokingHelpers.java | 87 +++++++++++++++++-- .../engine/reflection/TypedFunction.java | 41 --------- 7 files changed, 89 insertions(+), 260 deletions(-) delete mode 100644 engine/src/main/java/org/terasology/engine/reflection/CaptureSerializedOutput.java delete mode 100644 engine/src/main/java/org/terasology/engine/reflection/ExtractSerializedLambda.java delete mode 100644 engine/src/main/java/org/terasology/engine/reflection/InvokeWriteReplace.java delete mode 100644 engine/src/main/java/org/terasology/engine/reflection/TypedFunction.java 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 index fdd9fa0094e..3e9f3513273 100644 --- a/engine-tests/src/test/java/org/terasology/engine/reflection/TestInjectInvoke.java +++ b/engine-tests/src/test/java/org/terasology/engine/reflection/TestInjectInvoke.java @@ -12,7 +12,6 @@ import java.util.Properties; import static com.google.common.truth.Truth.assertThat; -import static org.terasology.engine.reflection.InvokingHelpers.argType; public class TestInjectInvoke { Integer red(String s) { @@ -37,45 +36,17 @@ InvokingContext makeContext() { return context; } - @Test - void canGetTypeOfFunctionArgument() { - assertThat(argType(this::red)).isAssignableTo(String.class); - assertThat(argType(this::green)).isAssignableTo(File.class); - } - - @Test - void canGetTypeOfFunctionArgumentThenDoStuffIfUsingReturnedFunction() throws URISyntaxException { - var r = TypedFunction.of(this::green); - URI greenResult = r.apply(new File("/dev/null")); - assertThat(greenResult).isEqualTo(new URI("file:/dev/null")); - } - - @Test - void canSupplyArgumentFromContext() throws URISyntaxException { - var context = makeContext(); - - var greenResult = context.invoke(this::green); - assertThat(greenResult).isEqualTo(new URI("file:/dev/null")); - assertThat(greenResult.getScheme()).isEqualTo("file"); - - // TODO: That worked but this fails. JVM bug? - // --debug=verboseResolution=all looks okay on the compiler side, must be runtime. - // assertThat(context.invoke(this::green)).isEqualTo(new URI("file:/dev/null")); - - assertThat(context.invoke(this::red)).isEqualTo(8); - } - @Test void canSupplyArgumentFromContextUsingSerialization() throws URISyntaxException { var context = makeContext(); - assertThat(context.invokeS(this::red)).isEqualTo(8); - assertThat(context.invokeS(this::green)).isEqualTo(new URI("file:/dev/null")); + 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.invokeS(this::yellow)).isEqualTo("zero12345678"); + assertThat(context.invoke(this::yellow)).isEqualTo("zero12345678"); } } diff --git a/engine/src/main/java/org/terasology/engine/reflection/CaptureSerializedOutput.java b/engine/src/main/java/org/terasology/engine/reflection/CaptureSerializedOutput.java deleted file mode 100644 index 1a0eaafbe2f..00000000000 --- a/engine/src/main/java/org/terasology/engine/reflection/CaptureSerializedOutput.java +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2022 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 - -package org.terasology.engine.reflection; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.io.OutputStream; -import java.io.Serializable; -import java.io.UncheckedIOException; -import java.lang.invoke.SerializedLambda; -import java.util.ArrayList; -import java.util.List; - -/** - * This technique, obtuse as it is, does not require overriding anything marked private. - *

- * Does require {@link java.io.SerializablePermission SerializablePermission("enableSubstitution")}, - * as per {@link ObjectOutputStream#enableReplaceObject}. - */ -@SuppressWarnings("JavadocReference") -public class CaptureSerializedOutput implements ExtractSerializedLambda { - static SerializedLambda getSerializedLambdaFromOutputStream(Serializable obj) { - List outputObjects; - try (var output = new SpyingOutputObjectStream(new ByteArrayOutputStream())) { - try { - output.writeObject(obj); - } finally { - outputObjects = output.getEmittedObjects(); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - - return (SerializedLambda) outputObjects.get(0); - } - - @Override - public SerializedLambda of(Serializable lambda) { - return getSerializedLambdaFromOutputStream(lambda); - } - - static class SpyingOutputObjectStream extends ObjectOutputStream { - private final List emittedObjects = new ArrayList<>(); - - SpyingOutputObjectStream(OutputStream out) throws IOException { - super(out); - enableReplaceObject(true); - } - - @Override - protected Object replaceObject(Object obj) { - emittedObjects.add(obj); - // The user of this class only cares about the rewritten form of the object, - // not the bytestream. Returning null saves the serializer work and avoids - // potential serialization errors. - return null; - } - - public List getEmittedObjects() { - return List.copyOf(emittedObjects); - } - } -} diff --git a/engine/src/main/java/org/terasology/engine/reflection/ExtractSerializedLambda.java b/engine/src/main/java/org/terasology/engine/reflection/ExtractSerializedLambda.java deleted file mode 100644 index 01c987dda18..00000000000 --- a/engine/src/main/java/org/terasology/engine/reflection/ExtractSerializedLambda.java +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2022 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 - -package org.terasology.engine.reflection; - -import java.io.Serializable; -import java.lang.invoke.SerializedLambda; - -@FunctionalInterface -public interface ExtractSerializedLambda { - SerializedLambda of(Serializable lambda); -} diff --git a/engine/src/main/java/org/terasology/engine/reflection/InvokeWriteReplace.java b/engine/src/main/java/org/terasology/engine/reflection/InvokeWriteReplace.java deleted file mode 100644 index b593998a88d..00000000000 --- a/engine/src/main/java/org/terasology/engine/reflection/InvokeWriteReplace.java +++ /dev/null @@ -1,68 +0,0 @@ -// 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.SerializedLambda; -import java.lang.reflect.Method; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; - -/** - * 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. - */ -public class InvokeWriteReplace implements ExtractSerializedLambda { - static SerializedLambda getSerializedLambda(Serializable obj) throws IllegalAccessException { - Method writeReplace; - try { - writeReplace = obj.getClass().getDeclaredMethod("writeReplace"); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - try { - Object replacement = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { - writeReplace.setAccessible(true); - return writeReplace.invoke(obj); - }); - return (SerializedLambda) replacement; - } catch (PrivilegedActionException e) { - Throwables.throwIfUnchecked(e.getCause()); - Throwables.throwIfInstanceOf(e.getCause(), IllegalAccessException.class); - throw new RuntimeException(e); - } - } - - static SerializedLambda getSerializedLambdaUnchecked(Serializable obj) { - try { - return getSerializedLambda(obj); - } catch (IllegalAccessException e) { - Throwables.throwIfUnchecked(e); - throw new RuntimeException(e); - } - } - - @Override - public SerializedLambda of(Serializable lambda) { - 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 index ea0c9418b43..6adc4a23718 100644 --- a/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java +++ b/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java @@ -3,14 +3,10 @@ package org.terasology.engine.reflection; -import com.google.common.base.Throwables; import org.terasology.engine.context.Context; -import reactor.core.publisher.Flux; -import java.lang.invoke.MethodHandle; import java.lang.invoke.SerializedLambda; import java.util.List; -import java.util.function.Function; import static com.google.common.base.Verify.verify; @@ -22,14 +18,8 @@ public InvokingContext(Context inner) { this.inner = inner; } - @SafeVarargs // SafeVarargs requires `final` - final R invoke(Function func, T... ts) { - var clazz = InvokingHelpers.argType(func, ts); - return func.apply(inner.getValue(clazz)); - } - - R invokeS(InvokingHelpers.SerializableFunction func) { - SerializedLambda serializedLambda = InvokeWriteReplace.getSerializedLambdaUnchecked(func); + 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); @@ -38,31 +28,12 @@ R invokeS(InvokingHelpers.SerializableFunction func) { return func.apply(inner.getValue(clazz)); } - R invokeS(InvokingHelpers.SerializableBiFunction func) { - return invokeSerializedLambda(InvokeWriteReplace.getSerializedLambdaUnchecked(func)); - } - - R invokeSerializedLambda(SerializedLambda serializedLambda) { + 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. - MethodHandle mh; - try { - mh = MethodHandleAdapters.ofLambda(serializedLambda); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - - var args = Flux.fromArray(mh.type().parameterArray()) - .map(inner::getValue) - .collectList().block(); - - try { - @SuppressWarnings("unchecked") R result = (R) mh.invokeWithArguments(args); - return result; - } catch (Throwable e) { - Throwables.throwIfUnchecked(e); - throw new RuntimeException(e); - } + return InvokingHelpers.invokeProvidingParametersByType( + InvokingHelpers.getSerializedLambdaUnchecked(func), + inner::getValue); } /* *** Delegate to wrapped Context *** */ diff --git a/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java b/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java index 3bd0b1d9036..cf0b0a3c931 100644 --- a/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java +++ b/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java @@ -3,28 +3,78 @@ 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() { } /** - * The Class of the function's first argument. + * 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? *

- * The trick here is that even if you pass no {@code ts}, the method still receives a zero-length - * array object, and we can read the class of that. + * 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. */ - @SafeVarargs - static Class argType(Function func, T... ts) { - return TypedFunction.of(func, ts).getInputClass(); + public static SerializedLambda getSerializedLambda(Serializable obj) throws IllegalAccessException { + Method writeReplace; + try { + writeReplace = obj.getClass().getDeclaredMethod("writeReplace"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + try { + Object replacement = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + writeReplace.setAccessible(true); + return writeReplace.invoke(obj); + }); + return (SerializedLambda) replacement; + } catch (PrivilegedActionException e) { + Throwables.throwIfUnchecked(e.getCause()); + Throwables.throwIfInstanceOf(e.getCause(), IllegalAccessException.class); + throw new RuntimeException(e); + } } - static List> getLambdaParameters(SerializedLambda serializedLambda) { + 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() @@ -32,6 +82,29 @@ static List> getLambdaParameters(SerializedLambda serializedLambda) { 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 { } diff --git a/engine/src/main/java/org/terasology/engine/reflection/TypedFunction.java b/engine/src/main/java/org/terasology/engine/reflection/TypedFunction.java deleted file mode 100644 index 61034368eb3..00000000000 --- a/engine/src/main/java/org/terasology/engine/reflection/TypedFunction.java +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2022 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 - -package org.terasology.engine.reflection; - -import java.util.function.Function; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -class TypedFunction implements Function { - private final Class clazz; - private final Function func; - - TypedFunction(Class clazz, Function func) { - this.clazz = clazz; - this.func = func; - } - - @SafeVarargs - static TypedFunction of(Function func, T... ts) { - checkNotNull((Object) ts, "did not received typed varargs array"); - checkArgument(ts.length == 0, "Args to ts are ignored, do not supply them."); - @SuppressWarnings("unchecked") Class clazz = (Class) ts.getClass().getComponentType(); - if (clazz.equals(Object.class)) { - //noinspection ImplicitArrayToString - throw new RuntimeException(String.format( - "Looks like Object, which is more generic than you want. %s of %s", clazz, ts)); - } - return new TypedFunction<>(clazz, func); - } - - @Override - public R apply(T t) { - return func.apply(t); - } - - public Class getInputClass() { - return clazz; - } -} From 28164c405d0df3e0f72f960a8b8730a9340159f7 Mon Sep 17 00:00:00 2001 From: Kevin Turner <83819+keturn@users.noreply.github.com> Date: Wed, 18 May 2022 13:23:15 -0700 Subject: [PATCH 3/3] feat(InvokingContext): support up to eight parameters --- .../engine/reflection/InvokingContext.java | 33 +++++++++++++++++++ .../engine/reflection/InvokingHelpers.java | 23 ++++++++++--- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java b/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java index 6adc4a23718..657f239c666 100644 --- a/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java +++ b/engine/src/main/java/org/terasology/engine/reflection/InvokingContext.java @@ -10,6 +10,7 @@ import static com.google.common.base.Verify.verify; +@SuppressWarnings("checkstyle:MethodTypeParameterName") public class InvokingContext implements Context { private final Context inner; @@ -36,6 +37,38 @@ public R invoke(InvokingHelpers.SerializableBiFunction 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 diff --git a/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java b/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java index cf0b0a3c931..6f4b83ba5ff 100644 --- a/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java +++ b/engine/src/main/java/org/terasology/engine/reflection/InvokingHelpers.java @@ -49,16 +49,12 @@ public static SerializedLambda getSerializedLambda(Serializable obj) throws Ille Method writeReplace; try { writeReplace = obj.getClass().getDeclaredMethod("writeReplace"); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - try { Object replacement = AccessController.doPrivileged((PrivilegedExceptionAction) () -> { writeReplace.setAccessible(true); return writeReplace.invoke(obj); }); return (SerializedLambda) replacement; - } catch (PrivilegedActionException e) { + } catch (PrivilegedActionException | NoSuchMethodException e) { Throwables.throwIfUnchecked(e.getCause()); Throwables.throwIfInstanceOf(e.getCause(), IllegalAccessException.class); throw new RuntimeException(e); @@ -109,4 +105,21 @@ 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 { } }