Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: InvokingContext injects ::method references with values from the Context #5018

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
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,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");
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -82,7 +82,7 @@ private static class ContextImplementation implements Context {
private final Map<Class<?>, Object> map = Maps.newConcurrentMap();

@Override
public <T> T get(Class<? extends T> type) {
public <T> T get(Class<T> type) {
T result = type.cast(map.get(type));
if (result != null) {
return result;
Expand Down
14 changes: 12 additions & 2 deletions engine/src/main/java/org/terasology/engine/context/Context.java
Original file line number Diff line number Diff line change
@@ -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.
*
Expand All @@ -23,7 +25,15 @@ public interface Context {
/**
* @return the object that is known in this context for this type.
*/
<T> T get(Class<? extends T> type);
<T> T get(Class<T> type);

default <T> T getValue(Class<T> 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -13,7 +13,7 @@
public class ContextImpl implements Context {
private final Context parent;

private final Map<Class<? extends Object>, Object> map = Maps.newConcurrentMap();
private final Map<Class<?>, Object> map = Maps.newConcurrentMap();


/**
Expand All @@ -30,7 +30,7 @@ public ContextImpl() {
}

@Override
public <T> T get(Class<? extends T> type) {
public <T> T get(Class<T> type) {
if (type == Context.class) {
return type.cast(this);
}
Expand All @@ -41,7 +41,7 @@ public <T> T get(Class<? extends T> type) {
if (parent != null) {
return parent.get(type);
}
return result;
return null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright 2021 The Terasology Foundation
// Copyright 2022 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0
package org.terasology.engine.context.internal;

import org.terasology.engine.context.Context;

public class MockContext implements Context {
@Override
public <T> T get(Class<? extends T> type) {
public <T> T get(Class<T> type) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T, R> R invoke(InvokingHelpers.SerializableFunction<T, R> func) {
SerializedLambda serializedLambda = InvokingHelpers.getSerializedLambdaUnchecked(func);

List<Class<?>> params = InvokingHelpers.getLambdaParameters(serializedLambda);
verify(params.size() == 1, "Expected exactly one parameter, found %s", params);

@SuppressWarnings("unchecked") Class<T> clazz = (Class<T>) params.get(0);
return func.apply(inner.getValue(clazz));
}

public <T, U, R> R invoke(InvokingHelpers.SerializableBiFunction<T, U, R> 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 <T1, T2, T3, R> R invoke(InvokingHelpers.SerializableFunction3<T1, T2, T3, R> func) {
return InvokingHelpers.invokeProvidingParametersByType(
InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue);
}

public <T1, T2, T3, T4, R> R invoke(InvokingHelpers.SerializableFunction4<T1, T2, T3, T4, R> func) {
return InvokingHelpers.invokeProvidingParametersByType(
InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue);
}

public <T1, T2, T3, T4, T5, R> R invoke(InvokingHelpers.SerializableFunction5<T1, T2, T3, T4, T5, R> func) {
return InvokingHelpers.invokeProvidingParametersByType(
InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue);
}

public <T1, T2, T3, T4, T5, T6, R> R invoke(InvokingHelpers.SerializableFunction6<T1, T2, T3, T4, T5, T6, R> func) {
return InvokingHelpers.invokeProvidingParametersByType(
InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue);
}

public <T1, T2, T3, T4, T5, T6, T7, R> R invoke(
InvokingHelpers.SerializableFunction7<T1, T2, T3, T4, T5, T6, T7, R> func) {
return InvokingHelpers.invokeProvidingParametersByType(
InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue);
}

public <T1, T2, T3, T4, T5, T6, T7, T8, R> R invoke(
InvokingHelpers.SerializableFunction8<T1, T2, T3, T4, T5, T6, T7, T8, R> func) {
return InvokingHelpers.invokeProvidingParametersByType(
InvokingHelpers.getSerializedLambdaUnchecked(func), inner::getValue);
}

/* *** Delegate to wrapped Context *** */

@Override
public <T> T get(Class<T> type) {
return inner.get(type);
}

@Override
public <T, U extends T> void put(Class<T> type, U object) {
inner.put(type, object);
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Is that bad? You might think so, but {@link Serializable} documentation says that
* Serializable objects may have that method <em>even though</em> it is not defined in
* the interface. So it's probably okay?
* <p>
* <b>Alternatives?</b>
* <p>
* Are there APIs to get the replaced-for-serialization object without trying to do
* do {@link Method#setAccessible} on arbitrary objects?
* <p>
* 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.
* <p>
* It is also possible to run the object through an {@link ObjectOutputStream} and
* <a href="https://gist.github.com/keturn/180a57f2f6069470556137bd06b4025d">capture the result</a>.
* 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<Class<?>> getLambdaParameters(SerializedLambda serializedLambda) {
var methodType = MethodType.fromMethodDescriptorString(
serializedLambda.getImplMethodSignature(),
serializedLambda.getClass().getClassLoader()
);
return methodType.parameterList();
}

public static <R> R invokeProvidingParametersByType(SerializedLambda serializedLambda, Function<Class<?>, ?> 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<T, R> extends Function<T, R>, Serializable { }

interface SerializableBiFunction<T, U, R> extends BiFunction<T, U, R>, Serializable { }

interface SerializableFunction3<T1, T2, T3, R> extends Serializable,
reactor.function.Function3<T1, T2, T3, R> { }

interface SerializableFunction4<T1, T2, T3, T4, R> extends Serializable,
reactor.function.Function4<T1, T2, T3, T4, R> { }

interface SerializableFunction5<T1, T2, T3, T4, T5, R> extends Serializable,
reactor.function.Function5<T1, T2, T3, T4, T5, R> { }

interface SerializableFunction6<T1, T2, T3, T4, T5, T6, R> extends Serializable,
reactor.function.Function6<T1, T2, T3, T4, T5, T6, R> { }

interface SerializableFunction7<T1, T2, T3, T4, T5, T6, T7, R> extends Serializable,
reactor.function.Function7<T1, T2, T3, T4, T5, T6, T7, R> { }

interface SerializableFunction8<T1, T2, T3, T4, T5, T6, T7, T8, R> extends Serializable,
reactor.function.Function8<T1, T2, T3, T4, T5, T6, T7, T8, R> { }
}
Loading