Планетная система Java

4 февраль, 2010 - 16:19Виктор Вейтман

Все течет, все изменяется, а в сфере информационных технологий это происходит особенно быстро. Еще совсем недавно казалось, что выбор Parrot в качестве промежуточного формата для интерпретируемых языков – дело решенное. Сегодня же на эту роль вполне реально претендует виртуальная машина Java.

Это началось три года назад: мгновение в истории человечества – чрезвычайно длительный период по меркам информационных технологий. Публикация JSR 223 (Java Specification Request – документа, который описывает спецификации или технологии, предлагаемые для включения в состав Java-платформы), посвященного взаимодействию между Java и языками сценариев, стала первым шагом долгого пути, на котором не было недостатка ни в интересных находках, ни в серьезных проблемах.

Игнорировать существующие программы невозможно

Ни у кого не возникает сомнений в том, что Java – очень мощный инструмент для создания приложений. Мощный, но далеко не единственный. В веб-разработках широко используются различные языки сценариев. Лидер среди них по объему (но не по качеству) написанного кода, безусловно, PHP, несколько отстают Python и Ruby. Многие программы искусственного интеллекта основаны на диалектах Lisp и Prolog. Все чаще применяются функциональные языки, такие как Haskell и Erlang.

Несмотря на уже созданный громадный объем кода, нередко программист, использующий Java, вынужден повторно решать задачу, которая давно реализована в виде работающей программы. Виной тому – набившая оскомину проблема совместимости, на этот раз между Java и языками сценариев. Неудивительно, что на очередном совещании создателей инструментальных средств вновь прозвучало слово «интеграция». А из множества способов был выбран, конечно же, тот, который может принести максимум пользы именно Java-разработчикам – выполнение сценариев, написанных на разных языках, под управлением виртуальной машины Java.

Компилятор – это еще не все

Казалось бы, нет ничего проще: разрабатываем компилятор, преобразующий исходный текст на выбранном нами языке, например Python, в байтовый код Java и обеспечиваем тем самым совместимость между этими языками. Увы, сюрпризы, которые готовит нам жизнь, не всегда приятны. Оказывается, преобразовать Java-программу в байтовый код можно, а программу на Python (а также Ruby, JavaScript, PHP и многих других языках) – нельзя. Почему? Этот вопрос требует подробного рассмотрения.

Ниже приведен чрезвычайно простой фрагмент Java-кода. Метод add() настолько тривиален, что непригоден ни для чего, кроме демонстрации проблем, однако эту единственную зада-чу он выполняет вполне исправно:

private static int add(int a, int b) {
return a+b;
}
...
System.out.println(add(2,3));
...

Компилируем исходный файл и получаем файл класса, в котором нас в основном интересует лишь одна строка – вызов метода add():

...
5: invokestatic #3; //Method add:(II)I
...

Формат представления операнда не очень информативен, однако комментарии дают нам все необходимое. В данном случае с помощью команды invokestatic вызывается метод add() с двумя операндами, каждый из них имеет тип int. Того же типа и значение, возвращаемое методом. Все вполне предсказуемо, именно так и был определен метод add() в исходном коде. Да и что непредвиденного может быть в языке, на котором созданы, наверное, терабайты различных программ! Неожиданности начнутся тогда, когда мы попробуем написать такой же фрагмент кода на языке Python:

def add(a,b) :
return(a+b);
......
print(add(2,3));
...

Несмотря на то что программа вполне работоспособна (в среде Python!), в заголовке метода add(a,b) нет никакого намека на типы ни операндов, ни возвращаемого значения. Они определятся позже, на этапе выполнения программы. В отличие от Java, где используется статическая типизация, Python – язык с динамической типизацией. Таким образом, вместо команды print(add(2,3)), которая выводит на экран значение 5, можно написать print(add(«abc», «def»)) и получить в результате строку «abcdef». В Java подобные трюки невозможны.

Как же создать байтовый код для такой программы? Формально мы были бы вынуждены описать метод add() как имеющий два операнда, типы которых будут уточнены позже, и возвращающий значение, тип которого на данный момент также неизвестен. Предсказывать ответ виртуальной машины на такое описание не имеет смысла по одной причине: мы попросту не сможем его составить, так как в байтовом коде Java нет соответствующих выразительных средств.

Вывод очевиден: как бы мы ни старались написать компилятор с языка с динамической типизацией, наши усилия останутся тщетными до тех пор, пока мы не изменим саму виртуальную машину Java.

JSR 292 предлагает решение

На данный момент в байтовом коде Java предусмотрены четыре инструкции, предназначенные для вызова процедур:

  • invokevirtual – вызов метода класса. Именно она чаще всего применяется при обращении к методам, определяемым в программе;
  • invokeinterface – вызов метода, объявленного в интерфейсе;
  • invokestatic – вызов статического метода;
  • invokespecial – вызов процедуры без указания типа возвращаемого значения. Такая команда чаще всего используется для обращения к конструкторам класса.

Недостает одной инструкции, которая вызывала бы метод, не указывая не только тип возвращаемого значения, но и типы операндов – под названием invokedynamic она была предложена именно в JSR 292. Эта команда свободна от ограничений, связанных с проверкой типов, и способна обеспечить вызов методов, определенных в языках с динамической типизацией. Однако за все надо платить. В данном случае платой является сложная процедура динамического связывания метода с другими элементами программы, для чего предусмотрен специальный пакет java.dyn, основными классами которого являются MethodHandle, Dynamic и InvokeDynamic. Ограниченный объем статьи не позволяет рассмотреть их работу даже вкратце, да и вряд ли это нужно. Во-первых, для того чтобы использовать инструментальные средства программирования, необязательно знать детали их реализации, а во-вторых, на эту тему есть множество публикаций, доступных всем желающим, например blogs.sun.com/jrose/entry/method_handles_in_a_nutshell.

Interface Injection

Казалось бы, компромисс между статической и динамической типизацией найден и проблема интеграции языков решена. К сожалению, это не так. В действительности инструкция invokedynamic способна обеспечить функционирование лишь программ с кодом, остающимся в процессе работы неизменным.

Наверное, каждый программист, использовавший ассемблер, задумывался, а не написать ли программу, которая будет переписывать сама себя. Многие не устояли перед искушением и помнят восторг от того, что после нескольких неудачных попыток такая программа наконец заработала. Языки сценариев, конечно, не ассемблер, но тем не менее многие из них содержат средства, позволяющие программе модифицировать собственный код. Например, в JavaScript каждый объект является массивом, и, определив в процессе работы очередной его элемент, можно включить в состав объекта дополнительное свойство или функцию.

И снова не обойтись без модификации виртуальной машины, и снова решение предлагает уже знакомый нам документ JSR 292. Основная идея состоит в том, что не следует изменять структуру класса, достаточно уточнить его принадлежность к той или иной категории, и она была воплощена в инжекции интерфейсов – механизме, позволяющем связать с существующим классом новый интерфейс, который не был связан с ним на этапе компиляции. Необходимость в инжекции возникает в трех случаях:

  • когда при обращении к объекту вызывается метод интерфейса;
  • когда производится сравнение с типом интерфейса посредством команды instanceof;
  • когда средства отражения (reflection) используются для того, чтобы проверить, реализуется ли интерфейс классом.

В процессе инжекции в первую очередь вызывается статический инжекционный метод, который проверяет, имеет ли класс право реализовать данный интерфейс, и если проверка дает положительный результат, предоставляет классу реализации методов, объявленных в интерфейсе. Руководитель экспертной группы JSR 292 Джон Роуз (John Rose) предложил использовать в качестве ссылки на реализацию метода специальную легковесную структуру под названием дескриптор метода. Эта структура позволяет обращаться к методу, не зная его имени, и в то же время обеспечивает производительность, ненамного отличающуюся от характерной для статических вызовов.

Инжекция интерфейса обеспечивает решение многих проблем, связанных с динамической модификацией кода, но, увы, не всех. Так, в языке JavaScript есть достаточно широко используемая функция eval(), в качестве параметра которой передается строка, содержащая любой JavaScript-код. Задача ее поддержки тривиальна для интерпретатора и непосильна для компилятора. Не поможет и инжекция интерфейса. Как быть? Время от времени встречаются высказывания о том, что возможность выполнять произвольную строку кода, сформированную в процессе функционирования программы, представляет собой серьезную угрозу безопасности, и средства, подобные функции eval(), следует запретить, однако большинство разработчиков хранят по этому поводу молчание. По-видимому, единое мнение еще не выработано.

Подождем до весны или...

Итак, основные подходы к интеграции Java с языками сценариев определены и скоро станут доступны разработчикам – в рамках Java 7, ожидаемой весной нынешнего года. Но что же делать программистам? Отложить свои планы на несколько месяцев и дожидаться новой версии Java? К счастью, отсутствие invokedynamic и инжекции интерфейсов – не препятствие для выполнения программ на языках с динамической типизацией. Работа по интеграции языков сценариев и Java ведется уже несколько лет, и на сегодняшний день данная виртуальная машина поддерживает более двадцати языков. Уже сейчас вокруг «звезды» Java вращаются «планеты», среди которых Jython (Python для Java), JRuby, PHP, Scala и многие другие. Это стало возможно благодаря применению процессоров сценариев, специальных классов, реализующих интерфейс ScriptEngine, каждый из которых выполняет интерпретацию кода на одном конкретном языке. Все, что необходимо сделать, – это установить программное обеспечение, предназначенное для его поддержки и указать в составе путей к классам соответствующий jar-файл. А для JavaScript и того не надо: средства поддержки этого языка изначально включены в состав JDK 6. Разработчику, которому потребуется выполнить JavaScript-сценарий в среде Java, достаточно сделать следующее:

  • создать экземпляр класса ScriptEngineManager;
  • получить процессор сценариев, вызвав метод getEngine-ByName() (подойдут также методы getEngineByExten-sion() и getEngineByMime Type());
  • сформировать строку, содержащую текст сценария;
  • передать сформированную строку методу eval() процессора сценариев (не путать с функцией eval() самого JavaScript!).

Подобным образом можно выполнить фрагмент кода на другом языке, например JRuby, однако в этом случае задача немного усложняется. Чтобы объект ScriptEngineManager действительно вернул процессор сценариев для JRuby, необходимо установить соответствующее ПО (www.jruby.org/download) и указать в переменной окружения CLASSPATH путь к библиотеке jruby.jar. (Ранее нужно было использовать также файл asm-2.2.3.jar, но в новой библиотеке все необходимые классы объединены в одном архивном файле).

Здесь читатель может задаться вопросом: если все и так работает, зачем же было затрачивать огромные усилия на создание invokedynamic и механизма инжекции интерфейсов? Работать-то работает, но далеко не идеально. Главный недостаток существующей реализации – чрезвычайно низкая производительность. Каждый процессор сценариев представляет собой «классический» интерпретатор, который при выполнении каждой команды осуществляет ее синтаксический разбор. Например, JavaScript-программа выполняется в несколько тысяч (!) раз медленнее, чем та же программа, написанная на языке Java. Виной тому – недопустимо малое быстродействие процессора Rhino – JavaScript-интерпретатора, встроенного в JDK 6. Когда же будут завершены планируемые работы по модификации виртуальной машины Java, скорость выполнения программ на языках сценариев если не сравняется, то по крайней мере станет соизмеримой со скоростью выполнения Java-кода.

Итак, «светлое будущее» для программистов, собирающихся использовать языки с динамической типизацией на платформе Java, не за горами. А пока оно не наступило, в распоряжении разработчиков есть процессоры сценариев, мощность которых маловата для практического применения, но вполне достаточна для отработки основных программных решений.