Создаем eval через байт-код JVM
В интерпретируемых языках программирования (или в языках, которые включают возможность компиляции в runtime) есть возможность вычисления значения выражения, полученного из внешнего источника (например, для построения графика функции, введенной пользователем) с подстановкой актуальных значений переменных. Но JVM изначально ориентирована на компиляцию перед выполнением и механизма, аналогичного eval, в JVM нет (кроме, конечно, возможности использования Nashorn и eval в JavaScript-коде). В этой статье мы рассмотрим пример динамической генерации байт-кода из математического выражения.
Любое математическое выражение может быть преобразовано в последовательность операций над стеком, для этого начнем с простых преобразований математических операций без учета приоритета. Для простоты будем использовать только целочисленные операции (но доработка для использования float/double-значений не будет очень сложной и нужно будет только заменить тип операнда, например заменить IADD на FADD или DADD).
Например, операция 10+20 может быть записана в виде последовательности операций байт-кода — положить константу 10 в стек, положить константу 20 в стек, выполнить сложение, вернуть значение со стека как результат метода. Для генерации через ASM фрагмент может выглядеть следующим образом:
var newMethod = writer.visitMethod(ACC_PUBLIC, "eval", "()I", null, null); newMethod.visitLdcInsn(10); newMethod.visitLdcInsn(20); newMethod.visitInsn(IADD); newMethod.visitInsn(IRETURN); newMethod.visitMaxs(2, 1); //стек из двух значений, локальных переменных тоже 2 + this newMethod.visitEnd();
В общем виде можно использовать обратную польскую нотацию и преобразовывать ее в последовательность операций со стеком, например 10 + 20 + 30 можно преобразовать в одну операцию (+ 10 20 30), однако в JVM арифметические операции сложения, умножения, вычитания, деления и остатка от деления являются бинарными, поэтому потребуется дополнительное преобразование в (+ (+ 20 30) 10). Создадим абстракцию для хранения таких операций:
enum OperationType < ADD, SUB, MUL, DIV, >class Operation < OperationType type; Object operand1; Object operand2; Operation(OperationType type, Object operand1, Object operand2) < this.type = type; this.operand1 = operand1; this.operand2 = operand2; >>
Теперь выполним преобразование в стековую машину, последовательность операций может быть такой:
var newMethod = writer.visitMethod(ACC_PUBLIC, "sum", "()I", null, null); newMethod.visitLdcInsn(30); //операция 2 newMethod.visitLdcInsn(10); newMethod.visitLdcInsn(20); newMethod.visitInsn(IADD); //операция 1 newMethod.visitInsn(IADD); newMethod.visitInsn(IRETURN); newMethod.visitMaxs(3, 1); //стек из двух значений, локальных переменных тоже 2 + this newMethod.visitEnd();
Будем использовать рекурсивную трансляцию описаний бинарных операций в байт-код, для упрощения выделим отдельный класс для генерации конструктора и генерации байт-кода для бинарной математической операции:
class OperationBuilder < OperationBuilder(ClassWriter writer) < this.writer = writer; >ClassWriter writer; void createConstructor(String className) < //создаем public class Calculator extends java.lang.Object writer.visit(V11, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); //создаем конструктор без аргументов var newConstructor = writer.visitMethod(ACC_PUBLIC, "", "()V", null, null); newConstructor.visitVarInsn(ALOAD, 0); //вызываем конструктор родительского класса (не интерфейс, поэтому false последний аргумент) //()V - сигнатура метода без аргументов и с типом результата void (V) newConstructor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); newConstructor.visitInsn(RETURN); //определяем размер стека и локальных переменных newConstructor.visitMaxs(1, 1); //завершаем создание конструктора newConstructor.visitEnd(); > void createMethod(String methodName, Operation root) < var newMethod = writer.visitMethod(ACC_PUBLIC, methodName, "()I", null, null); applyOperation(newMethod, root); newMethod.visitInsn(IRETURN); newMethod.visitMaxs(3, 1); //стек из двух значений, локальных переменных тоже 2 + this newMethod.visitEnd(); >void saveToClassFile(String className) throws IOException < var classFile = writer.toByteArray(); var stream = new FileOutputStream("build/classes/java/main/"+className+".class"); stream.write(classFile); stream.close(); >void applyOperation(MethodVisitor methodVisitor, Operation operation) < if (operation.operand1.getClass()==Operation.class) < applyOperation(methodVisitor, (Operation)operation.operand1); >else < methodVisitor.visitLdcInsn(operation.operand1); >if (operation.operand2.getClass()==Operation.class) < applyOperation(methodVisitor, (Operation)operation.operand2); >else < methodVisitor.visitLdcInsn(operation.operand2); >switch (operation.type) < case ADD ->methodVisitor.visitInsn(IADD); case SUB -> methodVisitor.visitInsn(ISUB); case MUL -> methodVisitor.visitInsn(IMUL); case DIV -> methodVisitor.visitInsn(IDIV); > > >
Теперь сделаем преобразование математического выражения, для правильного вычисления операций с одним приоритетом будем использовать последовательное выполнение бинарных операций, например 10+20+30 преобразуется в последовательность добавления значения констант в стек с последующим сложением. Но для генерации из математического выражения нужно будет также предусмотреть приоритеты операций и сначала подготовим наше выражение и добавим дополнительные скобки вокруг операций с более высоким приоритетом.
Для решения этой задачи будем добавлять рекурсивно дополнительные скобки вокруг выражений, приоритет которых выше, чем приоритет ранее обнаруженных операций. Например, 10+20*30-40 будет преобразован в 10+(20*30)-40 . Это позволит выполнять операции над стеком без учета приоритета, при этом выражения в скобках будут выполняться как отдельный набор операций, который возвращает результат в стек. При обнаружении скобок приоритет сбрасывается и значение в скобках рассматривается как отдельная подстрока:
public ElementResult expandBrackets(String current, int index, int priority) < StringBuilder builder = new StringBuilder(); while (index//начало подвыражения в скобках if (current.charAt(index)=='(') < var result = expandBrackets(current, index+1, 1); index = result.position; builder.append(result.result); continue; >//начало последнего обнаруженного числа int lastPosition = index; //пропускаем число while (index= '0' && current.charAt(index) if (myPriority > priority) < //если больше - оборачиваем в скобки (возможно в несколько пар, если разница приоритетов >1) ElementResult subitem = expandBrackets(current, lastPosition, myPriority); index = subitem.position; String result = subitem.result; for (int i = priority; i < myPriority; i++) result = "(" + result + ")"; builder.append(result); >else if (myPriority < priority) < //если меньше - просто добавляем число и выходим (продолжаем в предыдущих скобках) builder.append(current.substring(lastPosition, index)); return new ElementResult(builder.toString(), index); >else < //иначе добавляем к линейной последовательности операторов builder.append(current.substring(lastPosition, index)); if (indexelse < return new ElementResult(builder.toString(), index); >index++; > > return new ElementResult(builder.toString(), index); >
Теперь полученную строку будем использовать для генерации последовательности команд размещения констант в стек и выполнения вычислений над стеком:
void createMethod(String methodName, String math) < var newMethod = writer.visitMethod(ACC_PUBLIC, methodName, "()I", null, null); applyMethod(newMethod, math, 0); newMethod.visitInsn(IRETURN); newMethod.visitMaxs(10, 1); //стек из группы значений, локальных переменных тоже 2 + this newMethod.visitEnd(); >OperationType getType(char symbol) < OperationType ot = null; switch (symbol) < case '+' ->ot = ADD; case '-' -> ot = SUB; case '*' -> ot = MUL; case '/' -> ot = OperationType.DIV; > return ot; > void putOperationIfNeeded(MethodVisitor methodVisitor, OperationType operation) < if (operation!=null) < System.out.println("Push operation " + operation.name()); switch (operation) < case ADD ->methodVisitor.visitInsn(IADD); case SUB -> methodVisitor.visitInsn(ISUB); case MUL -> methodVisitor.visitInsn(IMUL); case DIV -> methodVisitor.visitInsn(IDIV); > > > int applyMethod(MethodVisitor visitor, String math, int currentIndex) < OperationType lastOperation = null; while (currentIndexif (math.charAt(currentIndex) == '(') < //скобки собираем как отдельное значение в стеке currentIndex = applyMethod(visitor, math, currentIndex + 1); putOperationIfNeeded(visitor, lastOperation); lastOperation=null; continue; >//сохраняем операцию для применения к следующему значению var type = getType(math.charAt(currentIndex)); if (type!=null) < lastOperation = type; currentIndex++; continue; >//извлекаем значение числового операнда StringBuilder number = new StringBuilder(); while (currentIndex < math.length() && math.charAt(currentIndex) >= '0' && math.charAt(currentIndex) System.out.println("Push constant "+number); visitor.visitLdcInsn(Integer.parseInt(number.toString())); //применяем операцию над стеком (если необходимо) putOperationIfNeeded(visitor, lastOperation); //и готовимся к следующей операции lastOperation = null; > return currentIndex; >
Для корректного создания метода также необходимо предусмотреть вычисление глубины стека (передается в visitMaxs ), для этого будем сохранять текущее значение глубины и увеличивать его на единицу при сохранении константы (или в дальнейшем добавим переменные) или уменьшать на два при выполнении бинарной операции (извлекается два значения, размещает один результат):
int maxDepth = 0; int depth = 0; void changeDepth(int delta) < depth+=delta; if (depth>maxDepth) < maxDepth = depth; >>
Для тестирования нужно будет создать объекта класса OperationBuilder , сгенерировать конструктор, метод eval и сохранить это в .class-файл:
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException
После того, как мы создали интерпретатор константных значений, мы можем добавить поддержку переменных. Здесь есть несколько вариантов решения, но мы ограничимся добавлением аргументов к функции в соответствии с обнаруженными ссылками на переменные, для этого мы должны будем изменить сигнатуру функции (добавить необходимое количество букв I для передачи целочисленных аргументов), использовать ее при обнаружении функции в getMethod(«eval», Integer.class) и вместо подстановки значения константы использовать обращение к локальной переменной из фрейма метода со смещением, который совпадает с обнаруженной буквой названия переменной. Полный текст eval с поддержкой переменных представлен в https://github.com/dzolotov/bytecode (ветка eval).
Материал подготовлен в преддверии старта онлайн-курса «Java Developer. Professional».
Недавно в рамках курса прошел открытый урок, посвященный разбору протокола HTTP. На уроке рассмотрели, что он из себя представляет, а для закрепления материала написали простейшие HTTP клиент и сервер на java.io. Если интересно, посмотреть запись урока можно на странице курса по ссылке.
Scripting in Java Tutorial — Scripting in Java eval
A ScriptEngine can execute a script in a String and a java.io.Reader.
By using Reader, we can execute a script on the network or in a file.
eval() method from the ScriptEngine interface has the following overloaded versions.
Object eval(String script) Object eval(Reader reader) Object eval(String script, Bindings bindings) Object eval(Reader reader, Bindings bindings) Object eval(String script, ScriptContext context) Object eval(Reader reader, ScriptContext context)
Example
The following code shows how to run Javascript script code stored from a .js file.
The contents of .js file named helloscript.js is listed as follows.
// Print a message print('Hello from JavaScript!');
Here is the code to run the script.
import java.io.IOException; import java.io.Reader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; /* w w w.ja v a2 s .c o m*/ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; public class Main < public static void main(String[] args) < String scriptFileName = "c:/test.js"; Path scriptPath = Paths.get(scriptFileName); ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); try < Reader scriptReader = Files.newBufferedReader(scriptPath); engine.eval(scriptReader); >catch (IOException | ScriptException e) < e.printStackTrace(); >> >
The code above generates the following result.
Example 2
The eval() method from ScriptEngine returns the last value in the script as an Object.
import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; /*w w w .j ava 2 s. c o m*/ public class Main < public static void main(String[] args) throws ScriptException < ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); Object result = null; result = engine.eval("1 + 2;"); System.out.println(result); result = engine.eval("1 + 2; 3 + 4;"); System.out.println(result); result = engine.eval("1 + 2; 3 + 4; var v = 5; v = 6;"); System.out.println(result); result = engine.eval("1 + 2; 3 + 4; var v = 5;"); System.out.println(result); result = engine.eval("print(1 + 2)"); System.out.println(result); > >
The code above generates the following result.
Example 4
The following code shows how to pass a Result object to a script that populates the Result object with a value.
import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; /* w w w .j a v a2s.c om*/ public class Main < private int val = -1; public void setValue(int x) < val = x; >public int getValue() < return val; > public static void main(String[] args) throws ScriptException < ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); Main result = new Main(); engine.put("result", result); String script = "3 + 4; result.setValue(1);"; engine.eval(script); int returnedValue = result.getValue(); System.out.println("Returned value is " + returnedValue); > >
The code above generates the following result.
- Next »
- « Previous
java2s.com | © Demo Source and Support. All rights reserved.