All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fiolino.common.reflection.Reflection Maven / Gradle / Ivy

Go to download

General structure to easily create dynamic logic via MethodHandles and others.

There is a newer version: 1.0.10
Show newest version
package org.fiolino.common.reflection;

import java.lang.invoke.*;
import java.util.Arrays;
import java.util.concurrent.Semaphore;
import java.util.function.Function;

import static java.lang.invoke.MethodHandles.lookup;
import static java.lang.invoke.MethodHandles.publicLookup;
import static java.lang.invoke.MethodType.methodType;

/**
 * Static class for various reflection based methods.
 *
 * Created by Kuli on 6/17/2016.
 */
final class Reflection {

    private static final MethodHandle SYNC, CONSTANT_HANDLE_FACTORY, DROP_ARGUMENTS, SET_TARGET;

    static {
        try {
            SYNC = publicLookup().findStatic(MutableCallSite.class, "syncAll", methodType(void.class, MutableCallSite[].class));
            CONSTANT_HANDLE_FACTORY = publicLookup().findStatic(MethodHandles.class, "constant",
                    methodType(MethodHandle.class, Class.class, Object.class));
            DROP_ARGUMENTS = publicLookup().findStatic(MethodHandles.class, "dropArguments",
                    methodType(MethodHandle.class, MethodHandle.class, int.class, Class[].class));
            SET_TARGET = publicLookup().findVirtual(CallSite.class, "setTarget",
                    methodType(void.class, MethodHandle.class));
        } catch (NoSuchMethodException | IllegalAccessException ex) {
            throw new InternalError(ex);
        }
    }

    /**
     * Builds a Registry for a given target handle.
     *
     * Implementation note:
     * This works for all kinds of MethodHandles if the resulting handle has no parameters,
     * but it will only work for direct method handles if there are some parameter types.
     *
     * @param target This handle will be called only once per parameter value.
     * @param leadingParameters Some values that are being added to the target at first
     * @return A Registry holding handles with exactly the same type as the target type minus the leading parameters
     */
    static Registry buildFor(MethodHandle target, Object... leadingParameters) {
        int pCount = target.type().parameterCount() - leadingParameters.length;
        if (pCount < 0) {
            throw new IllegalArgumentException("Too many leading parameters for " + target);
        }
        switch (pCount) {
            case 0:
                // No args, so just one simple instance
                target = insertLeadingArguments(target, leadingParameters);
                return new OneTimeRegistryBuilder(target, false);
            case 1:
                // Maybe just some type with few possible values?
                Class singleArgumentType = target.type().parameterType(leadingParameters.length);
                if (singleArgumentType.isEnum() && singleArgumentType.getEnumConstants().length <= 64) {
                    // Then it can be mapped directly
                    target = insertLeadingArguments(target, leadingParameters);
                    return buildForEnumParameter(target, singleArgumentType.asSubclass(Enum.class));
                }
                if (singleArgumentType == boolean.class) {
                    target = insertLeadingArguments(target, leadingParameters);
                    return new BooleanMappingCache(target);
                }
            default:
                return MultiArgumentExecutionBuilder.createFor(target, leadingParameters);
        }
    }

    private static MethodHandle insertLeadingArguments(MethodHandle target, Object[] leadingParameters) {
        if (leadingParameters.length > 0) {
            target = MethodHandles.insertArguments(target, 0, leadingParameters);
        }
        return target;
    }

    private static > Cache buildForEnumParameter(MethodHandle target, Class enumType) {
        MethodHandle ordinal;
        try {
            ordinal = publicLookup().findVirtual(enumType, "ordinal", methodType(int.class));
        } catch (NoSuchMethodException | IllegalAccessException ex) {
            throw new InternalError("ordinal", ex);
        }
        int length = enumType.getEnumConstants().length;
        return createCache(target, ordinal, length, length, 1);
    }

    static Cache createCache(MethodHandle target, MethodHandle filter, int initialSize, int maximum, int stepSize) {
        if (initialSize < 0) {
            throw new IllegalArgumentException("" + initialSize);
        }
        if (maximum >= 0) {
            if (initialSize > maximum) {
                throw new IllegalArgumentException(initialSize + " > " + maximum);
            }
            if (initialSize == maximum) {
                return new ParameterToIntMappingCache(target, filter, maximum);
            }
        }
        return new ExpandableParameterToIntMappingCache(target, filter, initialSize, maximum, stepSize);
    }

    private static class BooleanMappingCache implements Cache {
        private final OneTimeExecution falseExecution, trueExecution;
        private final CallSite callSite;

        BooleanMappingCache(MethodHandle target) {
            if (target.type().parameterCount() != 1 || target.type().parameterType(0) != boolean.class) {
                throw new IllegalArgumentException(target.toString());
            }
            falseExecution = OneTimeExecution.createFor(target);
            trueExecution = OneTimeExecution.createFor(target);
            MethodHandle decider = createDecider(Registry::getAccessor);
            callSite = new ConstantCallSite(decider);
        }

        private MethodHandle createDecider(Function type) {
            return MethodHandles.guardWithTest(MethodHandles.identity(boolean.class),
                    type.apply(trueExecution), type.apply(falseExecution));
        }

        @Override
        public CallSite getCallSite() {
            return callSite;
        }

        @Override
        public void reset() {
            falseExecution.reset();
            trueExecution.reset();
        }
    }

    /**
     * Adds the index of the failed insertion to it.
     *
     * @param exceptionHandler (ArrayIndexOutOfBoundsException,int,<valueType[]>)MethodHandle
     * @return (ArrayIndexOutOfBoundsException,<valueTypes>)MethodHandle
     */
    static MethodHandle modifyExceptionHandler(MethodHandle exceptionHandler, MethodHandle filter, MethodType targetType) {
        return modifyExceptionHandler(exceptionHandler, filter, targetType, new Semaphore(1));
    }

    /**
     * Adds the index of the failed insertion to it.
     *
     * @param exceptionHandler (ArrayIndexOutOfBoundsException,int,<valueType[]>)MethodHandle
     * @return (ArrayIndexOutOfBoundsException,<valueTypes>)MethodHandle
     */
    static MethodHandle modifyExceptionHandler(MethodHandle exceptionHandler, MethodHandle filter, MethodType targetType, Semaphore semaphore) {
        Class[] targetParameters = targetType.parameterArray();
        int targetArgCount = targetParameters.length;
        int filterArgCount = filter == null ? 1 : filter.type().parameterCount();
        assert filterArgCount <= targetArgCount;
        Class[] innerParameters = new Class[targetArgCount+1];
        innerParameters[0] = ArrayIndexOutOfBoundsException.class;
        System.arraycopy(targetParameters, 0, innerParameters, 1, targetArgCount);
        int[] argumentIndexes = new int[filterArgCount + targetArgCount + 1];
        for (int i=1; i<=targetArgCount; i++) {
            if (i <= filterArgCount) {
                argumentIndexes[i] = i;
            }
            argumentIndexes[filterArgCount + i] = i;
        }

        MethodHandle h = applyFilter(exceptionHandler, filter, 1);
        h = h.asCollector(Object[].class, targetArgCount);
        Class[] allArguments = h.type().parameterArray();
        System.arraycopy(targetParameters, 0, allArguments, 1, filterArgCount);
        System.arraycopy(targetParameters, 0, allArguments, filterArgCount+1, targetArgCount);
        allArguments[0] = ArrayIndexOutOfBoundsException.class;
        h = h.asType(methodType(MethodHandle.class, allArguments));

        h = MethodHandles.permuteArguments(h, methodType(MethodHandle.class, innerParameters), argumentIndexes);
        return Methods.synchronizeWith(h, semaphore);
    }

    private static MethodHandle applyFilter(MethodHandle target, MethodHandle filter, int pos) {
        if (filter == null) return target;
        return MethodHandles.collectArguments(target, pos, filter);
    }

    /**
     * A Registry implementation that fetches accessor and updater from arrays of MethodHandles.
     */
    private static class ParameterToIntMappingCache implements Cache {
        private final CallSite callSite;
        private final MethodHandle target, filter, exceptionHandler;
        private final int maximum;

        /**
         * Creates the caching handle factory.
         *
         * @param target Use this as the called handle.
         * @param maximum The initial size of the array; this should be the expected maximum length of an average key set
         */
        ParameterToIntMappingCache(MethodHandle target, MethodHandle filter, int maximum) {
            checkTargetAndFilter(target, filter);
            this.target = target;
            this.filter = filter;
            this.maximum = maximum;
            MethodHandles.Lookup lookup = lookup();
            MethodHandle exceptionHandler;
            try {
                exceptionHandler = lookup.findStatic(lookup.lookupClass(), "outOfBounds", methodType(MethodHandle.class, Throwable.class, int.class, Object[].class, int.class));
            } catch (NoSuchMethodException | IllegalAccessException ex) {
                throw new InternalError(ex);
            }
            exceptionHandler = MethodHandles.insertArguments(exceptionHandler, 3, maximum - 1);
            this.exceptionHandler = modifyExceptionHandler(exceptionHandler, filter, target.type());
            callSite = new MutableCallSite(target.type());
            reset();
        }

        private MethodHandle createAccessorHandle() {
            MethodHandle[] executions = new MethodHandle[maximum];
            return createFullHandle(target, callSite, executions, filter, exceptionHandler);
        }

        @Override
        public CallSite getCallSite() {
            return callSite;
        }

        @SuppressWarnings("unused")
        private static MethodHandle outOfBounds(Throwable t, int index, Object[] values, int maximum) {
            throw new LimitExceededException("Argument value " + Arrays.toString(values) + " resolves to " + index + " which is beyond 0.." + maximum, t);
        }

        @Override
        public void reset() {
            callSite.setTarget(createAccessorHandle());
            if (callSite instanceof MutableCallSite) {
                MutableCallSite.syncAll(new MutableCallSite[] {
                        (MutableCallSite) callSite
                });
            }
        }
    }

    private static void fillAccessors(MethodHandle target, CallSite outerCallSite, MethodHandle[] executions) {
        int length = executions.length;
        for (int i=0; i < length; i++) {
            MethodHandle h = executions[i];
            if (h == null) {
                h = createArrayUpdatingHandle(target, outerCallSite, executions, i);
                executions[i] = h;
            }
        }
    }

    private static MethodHandle createArrayUpdatingHandle(MethodHandle target, CallSite outerCallSite, MethodHandle[] executions, int arrayIndex) {
        MethodHandle setTarget = MethodHandles.arrayElementSetter(MethodHandle[].class);
        setTarget = MethodHandles.insertArguments(setTarget, 0, executions, arrayIndex);
        setTarget = addMutableCallSiteSynchronization(setTarget, outerCallSite);
        return createSingleExecutionHandle(target, setTarget);
    }

    private static MethodHandle createAccessingHandle(MethodHandle target, CallSite outerCallSite, MethodHandle[] executions, MethodHandle filter) {
        fillAccessors(target, outerCallSite, executions);
        MethodHandle getFromArray = MethodHandles.arrayElementGetter(MethodHandle[].class).bindTo(executions);
        MethodHandle filtered = applyFilter(getFromArray, filter, 0);
        return Methods.acceptThese(filtered, target.type().parameterArray());
    }

    private static MethodHandle createFullHandle(MethodHandle target, CallSite outerCallSite, MethodHandle[] executions, MethodHandle filter, MethodHandle exceptionHandler) {
        MethodHandle primary = createAccessingHandle(target, outerCallSite, executions, filter);
        MethodHandle handleGetter = MethodHandles.catchException(primary, ArrayIndexOutOfBoundsException.class, exceptionHandler);
        MethodHandle invoker = MethodHandles.exactInvoker(target.type());
        return MethodHandles.foldArguments(invoker, handleGetter);
    }

    private static void checkTargetAndFilter(MethodHandle target, MethodHandle filter) {
        if (filter != null && target.type().parameterCount() < filter.type().parameterCount()) {
            throw new IllegalArgumentException("Filter " + filter + " accepts more arguments than " + target);
        }
    }

    private static class ExpandableParameterToIntMappingCache implements Cache {
        private final MethodHandle target, filter;
        private final CallSite callSite;
        private final int maximumLength, stepSize;
        private final MethodHandle exceptionHandler;
        private final Semaphore semaphore;
        private MethodHandle[] executions;

        /**
         * Creates the caching handle factory.
         *
         * @param target Use this as the called handle.
         * @param initialMaximum The initial size of the array; this should be the expected maximum length of an average key set
         * @param finalMaximum This is the absolute maximum size; if this exceeds, an exception is thrown. Use this to limit
         *                     the amount of stored keys to avoid memory and performance issues
         */
        ExpandableParameterToIntMappingCache(MethodHandle target, MethodHandle filter, MutableCallSite callSite,
                                             int initialMaximum, int finalMaximum, int stepSize) {
            if (finalMaximum >= 0 && finalMaximum < initialMaximum) {
                throw new IllegalArgumentException(finalMaximum + " < " + initialMaximum);
            }
            checkTargetAndFilter(target, filter);
            this.callSite = callSite;
            this.target = target;
            this.filter = filter;
            this.maximumLength = finalMaximum;
            this.stepSize = stepSize;
            this.semaphore = new Semaphore(1);
            expandExecutions(0, initialMaximum);

            MethodHandle exceptionHandler;
            try {
                exceptionHandler = lookup().bind(this, "outOfBounds", methodType(MethodHandle.class, ArrayIndexOutOfBoundsException.class, int.class, Object[].class));
            } catch (NoSuchMethodException | IllegalAccessException ex) {
                throw new InternalError(ex);
            }
            this.exceptionHandler = modifyExceptionHandler(exceptionHandler, filter, target.type(), semaphore);

            updateCallSites();
        }

        /**
         * Creates the caching handle factory.
         *
         * @param target Use this as the called handle.
         * @param initialMaximum The initial size of the array; this should be the expected maximum length of an average key set
         * @param finalMaximum This is the absolute maximum size; if this exceeds, an exception is thrown. Use this to limit
         *                     the amount of stored keys to avoid memory and performance issues
         */
        ExpandableParameterToIntMappingCache(MethodHandle target, MethodHandle filter, int initialMaximum, int finalMaximum, int stepSize) {
            this(target, filter, new MutableCallSite(target.type()), initialMaximum, finalMaximum, stepSize);
        }

        private int expandExecutions(int length) {
            int l = executions.length;
            expandExecutions(l, length);
            return l;
        }

        private void expandExecutions(int start, int length) {
            if (start == 0) {
                executions = new MethodHandle[length];
            } else if (start < length) {
                executions = Arrays.copyOf(executions, length);
            } else {
                return;
            }
            for (int i=start; i < length; i++) {
                executions[i] = target;
            }
        }

        @Override
        public CallSite getCallSite() {
            return callSite;
        }

        @Override
        public void reset() {
            expandExecutions(0, maximumLength);
        }

        @SuppressWarnings("unused")
        private MethodHandle outOfBounds(ArrayIndexOutOfBoundsException ex, int index, Object[] arguments) {
            if (index < 0) {
                throw new LimitExceededException("" + index);
            }
            if (index < executions.length) {
                // If a concurrent call already expanded the array
                return executions[index];
            }
            if (maximumLength >= 0 && index > maximumLength) {
                throw new LimitExceededException(index + " > " + maximumLength);
            }
            int old = expandExecutions(index + stepSize);
            updateCallSites();
            return executions[index];
        }

        private void updateCallSites() {
            MethodHandle accessor = createFullHandle(target, callSite, executions, filter, exceptionHandler);
            callSite.setTarget(accessor);
        }
    }

    /**
     * Adds a call to MutableCallSite.syncAll() after the given handle if the callsite is a {@link MutableCallSite}.
     *
     * @param doBefore What will be done before
     * @param callSite The callsite to sync
     * @return A handle of the same type as doBefore
     */
    private static MethodHandle addMutableCallSiteSynchronization(MethodHandle doBefore, CallSite callSite) {
        if (callSite instanceof MutableCallSite) {
            MethodHandle syncOuterCallSite = SYNC.bindTo(new MutableCallSite[] {(MutableCallSite) callSite });
            syncOuterCallSite = Methods.acceptThese(syncOuterCallSite, doBefore.type().parameterArray());
            return MethodHandles.foldArguments(syncOuterCallSite, doBefore);
        }
        return doBefore;
    }

    /**
     * Creates a handle of type (<type.returnType>)MethodHandle that creates a constant handle returning the input value
     * returning the input of this factory.
     *
     * The created constant handle will be of the same type as the given type parameter.
     *
     * @param type The type of the new constant handle
     * @return the factory handle
     */
    private static MethodHandle createConstantHandleFactory(MethodType type) {
        Class returnType = type.returnType();
        MethodHandle constantHandleFactory = CONSTANT_HANDLE_FACTORY.bindTo(returnType).asType(methodType(MethodHandle.class, returnType));
        if (type.parameterCount() > 0) {
            MethodHandle dropArguments = MethodHandles.insertArguments(DROP_ARGUMENTS, 1, 0, type.parameterArray());
            constantHandleFactory = MethodHandles.filterReturnValue(constantHandleFactory, dropArguments);
        }

        return constantHandleFactory;
    }

    /**
     * Creates a handle that calls the given execution, and then creates a static constant handle that will be used
     * as a parameter in the given setTarget handle.
     *
     * @param execution A handle of arbitrary type, executing something
     * @param setTarget (MethodHandle)void -- sets the created constant handle
     * @return A handle of the same type as the execution handle
     */
    private static MethodHandle createSingleExecutionHandle(MethodHandle execution, MethodHandle setTarget) {
        MethodType type = execution.type();
        if (type.returnType() == void.class) {
            return createVoidSyncHandle(execution, setTarget);
        }

        MethodHandle constantHandleFactory = Reflection.createConstantHandleFactory(type);
        setTarget = MethodHandles.filterArguments(setTarget, 0, constantHandleFactory);

        MethodHandle setTargetAndReturnValue = Methods.returnArgument(setTarget, 0);
        MethodHandle result = MethodHandles.filterReturnValue(execution, setTargetAndReturnValue);
        if (execution.isVarargsCollector()) {
            result = result.asVarargsCollector(type.parameterType(type.parameterCount() - 1));
        }
        return result;
    }

    /**
     * Creates a handle that calld execution only once, and then changes the target to a no-op handle.
     *
     * @param execution What to do only once
     * @param setTarget (MethodHandle)void
     * @return A handle of the same type as execution
     */
    private static MethodHandle createVoidSyncHandle(MethodHandle execution, MethodHandle setTarget) {
        MethodHandle doNothing = Methods.acceptThese(Methods.DO_NOTHING, execution.type().parameterArray());
        MethodHandle setTargetToNoop = setTarget.bindTo(doNothing);

        // The following is only needed if execution accepts parameters, which is only the case when used from other MultiArgumentExecutionBuilder
        setTargetToNoop = Methods.acceptThese(setTargetToNoop, execution.type().parameterArray());
        return MethodHandles.foldArguments(setTargetToNoop, execution);
    }

    /**
     * Creates a handle that will call the given target exactly once, wrapped in a synchronized VolatileCallSite, so
     * that it's guaranteed that it will definitely be called only once.
     *
     * The return value will be stored in a ConstantHandle and pushed back into the given {@link CallSite}.
     *
     * @param target What to execute
     * @param callSite Where to put the constant value
     * @param semaphore Used to synchronize the execution
     * @return A handle of the same type as target
     */
    static MethodHandle createSingleExecutionWithSynchronization(MethodHandle target, CallSite callSite, Semaphore semaphore) {
        MethodHandle setTarget = SET_TARGET.bindTo(callSite);
        setTarget = addMutableCallSiteSynchronization(setTarget, callSite);
        return createSingleExecutionWithSynchronization(target, setTarget, semaphore);
    }

    /**
     * Creates a handle that will call the given target exactly once, wrapped in a synchronized VolatileCallSite, so
     * that it's guaranteed that it will definitely be called only once.
     *
     * The return value will be stored in a ConstantHandle and pushed back via the given setTarget handle.
     *
     * @param target What to execute
     * @param setTarget (MethodHandle)void -- will be called when the return value is set
     * @param semaphore Used to synchronize the execution
     * @return A handle of the same type as target
     */
    private static MethodHandle createSingleExecutionWithSynchronization(MethodHandle target, MethodHandle setTarget, Semaphore semaphore) {
        MethodType type = target.type();
        CallSite innerCallSite = new VolatileCallSite(type);
        MethodHandle setTargetInner = SET_TARGET.bindTo(innerCallSite);

        MethodHandle setBothTargets = MethodHandles.foldArguments(setTarget, setTargetInner);
        MethodHandle innerCaller = createSingleExecutionHandle(target, setBothTargets);
        innerCallSite.setTarget(innerCaller);
        MethodHandle guardedCaller = Methods.synchronizeWith(innerCallSite.dynamicInvoker(), semaphore);
        try {
            setTarget.invokeExact(guardedCaller);
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable t) {
            throw new RuntimeException("Exception when setting target", t);
        }

        MethodHandle resetInner = setTargetInner.bindTo(innerCaller);
        resetInner = Methods.acceptThese(resetInner, type.parameterArray());
        if (target.isVarargsCollector()) {
            resetInner = resetInner.asVarargsCollector(type.parameterType(type.parameterCount() - 1));
        }
        return MethodHandles.foldArguments(guardedCaller, resetInner);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy