com.offbynull.coroutines.instrumenter.OperandStackStateGenerators.java Source code

Java tutorial

Introduction

Here is the source code for com.offbynull.coroutines.instrumenter.OperandStackStateGenerators.java

Source

/*
 * Copyright (c) 2016, Kasra Faghihi, All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.offbynull.coroutines.instrumenter;

import com.offbynull.coroutines.instrumenter.asm.VariableTable.Variable;
import com.offbynull.coroutines.instrumenter.generators.DebugGenerators.MarkerType;
import static com.offbynull.coroutines.instrumenter.generators.DebugGenerators.debugMarker;
import static com.offbynull.coroutines.instrumenter.generators.GenericGenerators.merge;
import static com.offbynull.coroutines.instrumenter.generators.GenericGenerators.mergeIf;
import org.apache.commons.lang3.Validate;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.Frame;

/**
 * Utility class to generate bytecode instructions that save/load the operand stack.
 * @author Kasra Faghihi
 */
final class OperandStackStateGenerators {
    private OperandStackStateGenerators() {
        // do nothing
    }

    // A PRIMER ON HOW THIS CLASS WORKS...
    //
    // Thanks to the Frame object, we know how large the operand stack is and what type each position on the stack has. Imagine the
    // following operand stack.
    //
    // int3        <--- BOTTOM OF STACK
    // object2
    // int2
    // int1
    // object1     <--- TOP OF STACK
    //
    // 
    // TO SAVE...
    // ----------
    // We count the types on the stack and use that to determine the size of each storage array.
    //
    // int[]    = [0, 0, 0] (3 items)
    // float[]  = ignored (0 items)
    // long[]   = ignored (0 items)
    // double[] = ignored (0 items)
    // Object[] = [null, null] (2 items)
    //
    // Once we do that, we begin popping each item off the stack and storing it in to the appropiate storage array. Note that we start
    // at the end of each storage array and work our way down to index 0.
    //
    // AFTER POPPING object1 ... Object[] = [null,    object1]     int[] = [0,    0,    0]
    // AFTER POPPING int1    ... Object[] = [null,    object1]     int[] = [0,    0,    int1]
    // AFTER POPPING int2    ... Object[] = [null,    object1]     int[] = [0,    int2, int1]
    // AFTER POPPING object2 ... Object[] = [object2, object1]     int[] = [0,    int2, int1]
    // AFTER POPPING int3    ... Object[] = [object2, object1]     int[] = [int3, int2, int1]
    // 
    //
    // TO LOAD...
    // ----------
    // When we want to load back up what we saved, the process is essentially the reverse of saving. We use the Frame object again to
    // determine what types are being loaded back in, and we start reading items off the appropriate storage array and pushing them back on
    // to the stack.

    /**
     * Generates instructions to load the entire operand stack. Equivalent to calling
     * {@code loadOperandStack(markerType, storageVars, frame, 0, 0, frame.getStackSize())}.
     * @param markerType debug marker type
     * @param storageVars variables to load operand stack from
     * @param frame execution frame at the instruction where the operand stack is to be loaded
     * @return instructions to load the operand stack from the storage variables
     * @throws NullPointerException if any argument is {@code null}
     */
    public static InsnList loadOperandStack(MarkerType markerType, StorageVariables storageVars,
            Frame<BasicValue> frame) {
        return loadOperandStack(markerType, storageVars, frame, 0, 0, frame.getStackSize());
    }

    /**
     * Generates instructions to load a certain number of items to the top of the operand stack.
     * @param markerType debug marker type
     * @param storageVars variables to load operand stack from
     * @param frame execution frame at the instruction where the operand stack is to be loaded
     * @param storageStackStartIdx stack position where {@code storageVars} starts from
     * @param storageStackLoadIdx stack position where loading should start from
     * @param count number of stack items to load
     * @return instructions to load the operand stack from the specified storage variables
     * @throws NullPointerException if any argument is {@code null}
     * @throws IllegalArgumentException if any numeric argument is negative, or if you're trying to load stack items that aren't available
     * in the storage vars (stack items before {@code storageStackStartIdx}), or if you're trying to load too many items on the stack (such
     * that it goes past {@code frame.getStackSize()})
     */
    public static InsnList loadOperandStack(MarkerType markerType, StorageVariables storageVars,
            Frame<BasicValue> frame, int storageStackStartIdx, // stack idx which the storage was started at
            int storageStackLoadIdx, // stack idx we should start loading at
            int count) {
        Validate.notNull(markerType);
        Validate.notNull(storageVars);
        Validate.notNull(frame);
        // no negs allowed
        Validate.isTrue(storageStackStartIdx >= 0);
        Validate.isTrue(storageStackLoadIdx >= 0);
        Validate.isTrue(count >= 0);
        Validate.isTrue(storageStackLoadIdx >= storageStackStartIdx);
        Validate.isTrue(storageStackStartIdx + count <= frame.getStackSize());
        Validate.isTrue(storageStackStartIdx + count >= 0); // likely will never overflow unless crazy high count passedin, but just in case

        Variable intsVar = storageVars.getIntStorageVar();
        Variable floatsVar = storageVars.getFloatStorageVar();
        Variable longsVar = storageVars.getLongStorageVar();
        Variable doublesVar = storageVars.getDoubleStorageVar();
        Variable objectsVar = storageVars.getObjectStorageVar();

        int intsCounter = 0;
        int floatsCounter = 0;
        int longsCounter = 0;
        int doublesCounter = 0;
        int objectsCounter = 0;

        InsnList ret = new InsnList();

        // Increment offsets for parts of the storage arrays we don't care about. We need to do this so when we load we're loading from the
        // correct offsets in the storage arrays
        for (int i = storageStackStartIdx; i < storageStackLoadIdx; i++) {
            BasicValue basicValue = frame.getStack(i);
            Type type = basicValue.getType();

            // If type is 'Lnull;', this means that the slot has been assigned null and that "there has been no merge yet that would 'raise'
            // the type toward some class or interface type" (from ASM mailing list). We know this slot will always contain null at this
            // point in the code so there's no specific value to load up from the array.
            if (type.getSort() == Type.OBJECT && "Lnull;".equals(type.getDescriptor())) {
                continue; // skip
            }

            switch (type.getSort()) {
            case Type.BOOLEAN:
            case Type.BYTE:
            case Type.SHORT:
            case Type.CHAR:
            case Type.INT:
                intsCounter++;
                break;
            case Type.FLOAT:
                floatsCounter++;
                break;
            case Type.LONG:
                longsCounter++;
                break;
            case Type.DOUBLE:
                doublesCounter++;
                break;
            case Type.ARRAY:
            case Type.OBJECT:
                objectsCounter++;
                break;
            case Type.METHOD:
            case Type.VOID:
            default:
                throw new IllegalArgumentException();
            }
        }

        // Restore the stack
        ret.add(debugMarker(markerType, "Loading stack items"));
        for (int i = storageStackLoadIdx; i < storageStackLoadIdx + count; i++) {
            BasicValue basicValue = frame.getStack(i);
            Type type = basicValue.getType();

            // If type is 'Lnull;', this means that the slot has been assigned null and that "there has been no merge yet that would 'raise'
            // the type toward some class or interface type" (from ASM mailing list). We know this slot will always contain null at this
            // point in the code so there's no specific value to load up from the array. Instead we push a null in to that slot, thereby
            // keeping the same 'Lnull;' type originally assigned to that slot (it doesn't make sense to do a CHECKCAST because 'null' is
            // not a real class and can never be a real class -- null is a reserved word in Java).
            if (type.getSort() == Type.OBJECT && "Lnull;".equals(type.getDescriptor())) {
                ret.add(debugMarker(markerType, "Loading null value at " + i));
                ret.add(new InsnNode(Opcodes.ACONST_NULL));
                continue;
            }

            // Load item from stack storage array
            ret.add(debugMarker(markerType, "Loading from container at" + i));

            // Convert the item to an object (if not already an object) and stores it in local vars table. Item removed from stack.
            switch (type.getSort()) {
            case Type.BOOLEAN:
            case Type.BYTE:
            case Type.SHORT:
            case Type.CHAR:
            case Type.INT:
                ret.add(debugMarker(markerType, "Loading int at " + i + " from storage index " + intsCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, intsVar.getIndex())); // [int[]]
                ret.add(new LdcInsnNode(intsCounter)); // [int[], idx]
                ret.add(new InsnNode(Opcodes.IALOAD)); // [val]
                intsCounter++;
                break;
            case Type.FLOAT:
                ret.add(debugMarker(markerType, "Loading float at " + i + " from storage index " + floatsCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, floatsVar.getIndex())); // [float[]]
                ret.add(new LdcInsnNode(floatsCounter)); // [float[], idx]
                ret.add(new InsnNode(Opcodes.FALOAD)); // [val]
                floatsCounter++;
                break;
            case Type.LONG:
                ret.add(debugMarker(markerType, "Loading long at " + i + " from storage index " + longsCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, longsVar.getIndex())); // [long[]]
                ret.add(new LdcInsnNode(longsCounter)); // [long[], idx]
                ret.add(new InsnNode(Opcodes.LALOAD)); // [val_PART1, val_PART2]
                longsCounter++;
                break;
            case Type.DOUBLE:
                ret.add(debugMarker(markerType,
                        "Loading double at " + i + " from storage index " + doublesCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, doublesVar.getIndex())); // [double[]]
                ret.add(new LdcInsnNode(doublesCounter)); // [double[], idx]
                ret.add(new InsnNode(Opcodes.DALOAD)); // [val_PART1, val_PART2]
                doublesCounter++;
                break;
            case Type.ARRAY:
            case Type.OBJECT:
                ret.add(debugMarker(markerType,
                        "Loading object at " + i + " from storage index " + objectsCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, objectsVar.getIndex())); // [Object[]]
                ret.add(new LdcInsnNode(objectsCounter)); // [Object[], idx]
                ret.add(new InsnNode(Opcodes.AALOAD)); // [val]
                ret.add(new TypeInsnNode(Opcodes.CHECKCAST, basicValue.getType().getInternalName()));
                objectsCounter++;
                break;
            case Type.METHOD:
            case Type.VOID:
            default:
                throw new IllegalArgumentException();
            }
        }

        return ret;
    }

    /**
     * Generates instructions to save the entire operand stack. Equivalent to calling
     * {@code saveOperandStack(markerType, storageVars, frame, frame.getStackSize())}.
     * <p>
     * The instructions generated here expect the operand stack to be fully loaded. The stack items specified by {@code frame} must actually
     * all be on the operand stack.
     * <p>
     * REMEMBER: The items aren't returned to the operand stack after they've been saved (they have been popped off the stack). If you want
     * them back on the operand stack, reload using {@code loadOperandStack(markerType, storageVars, frame)}.
     * @param markerType debug marker type
     * @param storageVars variables to store operand stack in to
     * @param frame execution frame at the instruction where the operand stack is to be saved
     * @return instructions to save the operand stack to the storage variables
     * @throws NullPointerException if any argument is {@code null}
     */
    public static InsnList saveOperandStack(MarkerType markerType, StorageVariables storageVars,
            Frame<BasicValue> frame) {
        return saveOperandStack(markerType, storageVars, frame, frame.getStackSize());
    }

    /**
     * Generates instructions to save a certain number of items from the top of the operand stack.
     * <p>
     * The instructions generated here expect the operand stack to be fully loaded. The stack items specified by {@code frame} must actually
     * all be on the operand stack.
     * <p>
     * REMEMBER: The items aren't returned to the operand stack after they've been saved (they have been popped off the stack). If you want
     * them back on the operand stack, reload using
     * {@code loadOperandStack(markerType, storageVars, frame, frame.getStackSize() - count, frame.getStackSize() - count, count)}.
     * @param markerType debug marker type
     * @param storageVars variables to store operand stack in to
     * @param frame execution frame at the instruction where the operand stack is to be saved
     * @param count number of items to store from the stack
     * @return instructions to save the operand stack to the storage variables
     * @throws NullPointerException if any argument is {@code null}
     * @throws IllegalArgumentException if {@code size} is larger than the number of items in the stack at {@code frame} (or is negative),
     * or if {@code count} is larger than {@code top} (or is negative)
     */
    public static InsnList saveOperandStack(MarkerType markerType, StorageVariables storageVars,
            Frame<BasicValue> frame, int count) {
        Validate.notNull(markerType);
        Validate.notNull(storageVars);
        Validate.notNull(frame);
        Validate.isTrue(count >= 0);
        Validate.isTrue(count <= frame.getStackSize());

        Variable intsVar = storageVars.getIntStorageVar();
        Variable floatsVar = storageVars.getFloatStorageVar();
        Variable longsVar = storageVars.getLongStorageVar();
        Variable doublesVar = storageVars.getDoubleStorageVar();
        Variable objectsVar = storageVars.getObjectStorageVar();

        StorageSizes storageSizes = computeSizes(frame, frame.getStackSize() - count, count);

        int intsCounter = storageSizes.getIntsSize() - 1;
        int floatsCounter = storageSizes.getFloatsSize() - 1;
        int longsCounter = storageSizes.getLongsSize() - 1;
        int doublesCounter = storageSizes.getDoublesSize() - 1;
        int objectsCounter = storageSizes.getObjectsSize() - 1;

        InsnList ret = new InsnList();

        // Create stack storage arrays and save them
        ret.add(merge(debugMarker(markerType, "Saving operand stack (" + count + " items)"),
                mergeIf(storageSizes.getIntsSize() > 0, () -> new Object[] {
                        debugMarker(markerType, "Generating ints container (" + storageSizes.getIntsSize() + ")"),
                        new LdcInsnNode(storageSizes.getIntsSize()),
                        new IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_INT),
                        new VarInsnNode(Opcodes.ASTORE, intsVar.getIndex()) }),
                mergeIf(storageSizes.getFloatsSize() > 0,
                        () -> new Object[] {
                                debugMarker(markerType,
                                        "Generating floats container (" + storageSizes.getFloatsSize() + ")"),
                                new LdcInsnNode(storageSizes.getFloatsSize()),
                                new IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_FLOAT),
                                new VarInsnNode(Opcodes.ASTORE, floatsVar.getIndex()) }),
                mergeIf(storageSizes.getLongsSize() > 0, () -> new Object[] {
                        debugMarker(markerType, "Generating longs container (" + storageSizes.getLongsSize() + ")"),
                        new LdcInsnNode(storageSizes.getLongsSize()),
                        new IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_LONG),
                        new VarInsnNode(Opcodes.ASTORE, longsVar.getIndex()) }),
                mergeIf(storageSizes.getDoublesSize() > 0, () -> new Object[] {
                        debugMarker(markerType,
                                "Generating doubles container (" + storageSizes.getDoublesSize() + ")"),
                        new LdcInsnNode(storageSizes.getDoublesSize()),
                        new IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_DOUBLE),
                        new VarInsnNode(Opcodes.ASTORE, doublesVar.getIndex()) }),
                mergeIf(storageSizes.getObjectsSize() > 0,
                        () -> new Object[] {
                                debugMarker(markerType,
                                        "Generating objects container (" + storageSizes.getObjectsSize() + ")"),
                                new LdcInsnNode(storageSizes.getObjectsSize()),
                                new TypeInsnNode(Opcodes.ANEWARRAY, "java/lang/Object"),
                                new VarInsnNode(Opcodes.ASTORE, objectsVar.getIndex()) })));

        // Save the stack
        int start = frame.getStackSize() - 1;
        int end = frame.getStackSize() - count;
        for (int i = start; i >= end; i--) {
            BasicValue basicValue = frame.getStack(i);
            Type type = basicValue.getType();

            // If type is 'Lnull;', this means that the slot has been assigned null and that "there has been no merge yet that would 'raise'
            // the type toward some class or interface type" (from ASM mailing list). We know this slot will always contain null at this
            // point in the code so we can avoid saving it (but we still need to do a POP to get rid of it). When we load it back up, we can
            // simply push a null in to that slot, thereby keeping the same 'Lnull;' type.
            if ("Lnull;".equals(type.getDescriptor())) {
                ret.add(debugMarker(markerType, "Skipping null value at " + i));
                ret.add(new InsnNode(Opcodes.POP));
                continue;
            }

            // Convert the item to an object (if not already an object) and stores it in local vars table. Item removed from stack.
            switch (type.getSort()) {
            case Type.BOOLEAN:
            case Type.BYTE:
            case Type.SHORT:
            case Type.CHAR:
            case Type.INT:
                ret.add(debugMarker(markerType,
                        "Popping/storing int at " + i + " to storage index " + intsCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, intsVar.getIndex())); // [val, int[]]
                ret.add(new InsnNode(Opcodes.SWAP)); // [int[], val]
                ret.add(new LdcInsnNode(intsCounter)); // [int[], val, idx]
                ret.add(new InsnNode(Opcodes.SWAP)); // [int[], idx, val]
                ret.add(new InsnNode(Opcodes.IASTORE)); // []
                intsCounter--;
                break;
            case Type.FLOAT:
                ret.add(debugMarker(markerType,
                        "Popping/storing float at " + i + " to storage index " + floatsCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, floatsVar.getIndex())); // [val, float[]]
                ret.add(new InsnNode(Opcodes.SWAP)); // [float[], val]
                ret.add(new LdcInsnNode(floatsCounter)); // [float[], val, idx]
                ret.add(new InsnNode(Opcodes.SWAP)); // [float[], idx, val]
                ret.add(new InsnNode(Opcodes.FASTORE)); // []
                floatsCounter--;
                break;
            case Type.LONG:
                ret.add(debugMarker(markerType,
                        "Popping/storing long at " + i + " to storage index " + longsCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, longsVar.getIndex())); // [val_PART1, val_PART2, long[]]
                ret.add(new LdcInsnNode(longsCounter)); // [val_PART1, val_PART2, long[], idx]
                ret.add(new InsnNode(Opcodes.DUP2_X2)); // [long[], idx, val_PART1, val_PART2, long[], idx]
                ret.add(new InsnNode(Opcodes.POP2)); // [long[], idx, val_PART1, val_PART2]
                ret.add(new InsnNode(Opcodes.LASTORE)); // []
                longsCounter--;
                break;
            case Type.DOUBLE:
                ret.add(debugMarker(markerType,
                        "Popping/storing double at " + i + " to storage index " + doublesCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, doublesVar.getIndex())); // [val_PART1, val_PART2, double[]]
                ret.add(new LdcInsnNode(doublesCounter)); // [val_PART1, val_PART2, double[], idx]
                ret.add(new InsnNode(Opcodes.DUP2_X2)); // [double[], idx, val_PART1, val_PART2, double[], idx]
                ret.add(new InsnNode(Opcodes.POP2)); // [double[], idx, val_PART1, val_PART2]
                ret.add(new InsnNode(Opcodes.DASTORE)); // []
                doublesCounter--;
                break;
            case Type.ARRAY:
            case Type.OBJECT:
                ret.add(debugMarker(markerType,
                        "Popping/storing object at " + i + " to storage index " + objectsCounter));
                ret.add(new VarInsnNode(Opcodes.ALOAD, objectsVar.getIndex())); // [val, object[]]
                ret.add(new InsnNode(Opcodes.SWAP)); // [object[], val]
                ret.add(new LdcInsnNode(objectsCounter)); // [object[], val, idx]
                ret.add(new InsnNode(Opcodes.SWAP)); // [object[], idx, val]
                ret.add(new InsnNode(Opcodes.AASTORE)); // []
                objectsCounter--;
                break;
            case Type.METHOD:
            case Type.VOID:
            default:
                throw new IllegalArgumentException();
            }
        }

        // At this point, the storage array will contain the saved operand stack AND THE STACK WILL HAVE count ITEMS POPPED OFF OF IT.
        // 
        // Reload using...
        // ---------------
        // ret.add(debugMarker(markerType, "Reloading stack items"));
        // InsnList reloadInsnList = loadOperandStack(markerType, storageVars, frame,
        //         frame.getStackSize() - count,
        //         frame.getStackSize() - count,
        //         count);
        // ret.add(reloadInsnList);

        return ret;
    }

    /**
     * Compute sizes required for the storage arrays that will contain the operand stack at this frame.
     * @param frame frame to compute for
     * @param offset the position within the operand stack to start calculating
     * @param length the number of stack items to include in calculation
     * @return size required by each storage array
     * @throws NullPointerException if any argument is {@code null}
     * @throws IllegalArgumentException if any numeric argument is negative, or if {@code offset + length} is larger than the size of the
     * operand stack
     */
    public static StorageSizes computeSizes(Frame<BasicValue> frame, int offset, int length) {
        Validate.notNull(frame);
        Validate.isTrue(offset >= 0);
        Validate.isTrue(length >= 0);
        Validate.isTrue(offset < frame.getStackSize());
        Validate.isTrue(offset + length <= frame.getStackSize());

        // Count size required for each storage array
        int intsSize = 0;
        int longsSize = 0;
        int floatsSize = 0;
        int doublesSize = 0;
        int objectsSize = 0;
        for (int i = offset + length - 1; i >= offset; i--) {
            BasicValue basicValue = frame.getStack(i);
            Type type = basicValue.getType();

            // If type is 'Lnull;', this means that the slot has been assigned null and that "there has been no merge yet that would 'raise'
            // the type toward some class or interface type" (from ASM mailing list). We know this slot will always contain null at this
            // point in the code so we can avoid saving it. When we load it back up, we can simply push a null in to that slot, thereby
            // keeping the same 'Lnull;' type.
            if ("Lnull;".equals(type.getDescriptor())) {
                continue;
            }

            switch (type.getSort()) {
            case Type.BOOLEAN:
            case Type.BYTE:
            case Type.SHORT:
            case Type.CHAR:
            case Type.INT:
                intsSize++;
                break;
            case Type.FLOAT:
                floatsSize++;
                break;
            case Type.LONG:
                longsSize++;
                break;
            case Type.DOUBLE:
                doublesSize++;
                break;
            case Type.ARRAY:
            case Type.OBJECT:
                objectsSize++;
                break;
            case Type.METHOD:
            case Type.VOID:
            default:
                throw new IllegalStateException();
            }
        }

        return new StorageSizes(intsSize, longsSize, floatsSize, doublesSize, objectsSize);
    }
}