openjdk.ruOpenJDK на русском

JEP 276: Dynamic Linking of Language-Defined Object Models

Динамическая компоновка объектных моделей, определяемых языками

ОтветственныйAttila Szegedi
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск9
Компонентcore-libs / java.lang.invoke
Обсуждениеcore dash libs dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьM
РецензентыAlex Buckley, Jim Laskey
ОдобренBrian Goetz
Создан2015/03/24 17:45
Обновлён2017/05/17 00:59
Задача8075866

Аннотация

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

Цели

Основная цель — дать возможность компилировать высокоуровневые операции над объектами в выражениях, тип которых неизвестен во время компиляции, в инструкции INVOKEDYNAMIC. Например, obj.color превращается в INVOKEDYNAMIC "dyn:getProp:color". Во время выполнения предоставляемая инфраструктура будет передавать запрос на компоновку этих точек вызова набору компоновщиков. Эти компоновщики знают конкретные типы объектов и могут создавать дескрипторы методов с подходящей реализацией операций.

Эти механизмы служат основой для реализации сред выполнения программ на языках, в которых есть понятие объектных выражений с неизвестными во время компиляции типами и в которых нужно выражать типичные объектно-ориентированные операции над такими объектами.

Возможность объединять компоновщики нескольких сред выполнения языков обеспечивает разумную степень взаимодействия между несколькими средами выполнения языков, которые сосуществуют в одном процессе JVM. Если объект, принадлежащий одной среде выполнения, передаётся в другую среду выполнения, то точка вызова INVOKEDYNAMIC, созданная компилятором одного языка, должна компоноваться компоновщиком другого языка.

Что не является целью

Мы не хотим задавать семантику компоновки операций для какого-либо одного языка программирования или для какой-либо среды выполнения такого языка. Мы не будем менять описанный в JVMS механизм начальной загрузки (bootstrap) точки вызова, мы только используем его.

Мотивация

INVOKEDYNAMIC даёт основу на уровне JVM для компоновки методов, определяемой приложением. Но в нём нет способа выразить высокоуровневые операции над объектами или методы, которые их реализуют. Эти операции — обычный набор операций в объектно-ориентированных средах: доступ к свойствам, доступ к элементам коллекций, вызов конструкторов, вызов именованных методов (возможно, с множественной диспетчеризацией, например аналоги разрешения перегруженных методов Java во время компоновки и во время выполнения). Всё это функции, которые обычно нужны языку на JVM, но каждой реализации языка до сих пор приходилось изобретать их заново. Семантика таких операций в разных языках ожидаемо различается (у языков, в конце концов, должно быть что-то отличающее их, иначе это был бы один и тот же язык), но у всех них есть общая потребность: компоновка с объектами платформы Java (их часто называют «Plain Old Java Objects», то есть экземплярами классов Java, которые не принадлежат среде выполнения языка и не сгенерированы ею) так, чтобы семантика совпадала с их использованием в языке Java.

Nashorn успешно доказывает жизнеспособность этого подхода. Встретив выражение вида, например, obj.foo, компилятор байт-кода Nashorn может просто сгенерировать INVOKEDYNAMIC "dyn:getProp:foo", а затем во время выполнения поручить динамическому компоновщику предоставить реализацию чтения свойства в зависимости от того, чем окажется значение выражения: объектом JavaScript, простым Java-объектом или чем-то ещё.

Механизм компоновки динамического языка с Java даёт почти всё необходимое и для компоновки языка с самим собой, и Nashorn так и делает: в нём есть единый механизм компоновки, который передаёт управление либо собственному компоновщику, либо компоновщику «простых Java-объектов», в зависимости от ситуации. Более того, если у среды выполнения единая архитектура компоновки и с самой собой, и с Java-объектами, то возможность компоновки с объектами ещё одной среды выполнения языка в той же JVM фактически достаётся бесплатно, и так обеспечивается разумная степень межъязыкового взаимодействия.

Это ценная общая функциональность для реализации любой системы (например, среды выполнения динамического языка), которая компилирует в байт-код при некоторой неопределённости типов во время компиляции.

Описание

Рабочая реализация содержится в коде пакетов jdk.internal.dynalink.*, которые сейчас поставляются в JDK 8 как внутренняя зависимость Nashorn. Мы собираемся доработать его и открыть в виде набора пакетов с именами jdk.dynalink.*, размещённых в новом модуле jdk.dynalink.

Ниже описан текущий дизайн, и предполагается, что этот раздел будет меняться по ходу работы.

Операции

Операции над объектами выражаются инструкциями INVOKEDYNAMIC, имя которых описывает операцию.

Этот JEP определяет перечисленные ниже операции. У всех операций, определённых этим JEP, есть префикс «dyn:», и предполагается, что этот префикс будет зарезервирован для будущих расширений Dynalink.

Операции над свойствами объекта:

  • INVOKEDYNAMIC "dyn:getProp:<name>"(Object)Object используется для получения значения именованного свойства объекта. Типы в сигнатуре и здесь, и во всех остальных операциях могут быть более конкретными, чем Object (в том числе примитивными).
  • INVOKEDYNAMIC "dyn:getProp"(Object,Object)Object используется для получения значения именованного свойства объекта. Получатель передаётся первым аргументом, имя свойства — вторым. В отличие от предыдущей операции, имя здесь не фиксировано.
  • INVOKEDYNAMIC "dyn:setProp:<name>"(Object, Object)void используется для установки значения именованного свойства объекта. Первый аргумент — получатель, второй — устанавливаемое значение.
  • INVOKEDYNAMIC "dyn:setProp"(Object,Object,Object)void используется для установки значения именованного свойства объекта. Первый аргумент — получатель, второй — имя свойства, третий — устанавливаемое значение. В отличие от предыдущей операции, имя здесь не фиксировано.

Операции над элементами коллекций:

  • INVOKEDYNAMIC "dyn:getElem:<key>"(Object)Object используется для получения элемента объекта-коллекции (массива, списка, отображения и т. д.) по фиксированному ключу. В этой форме ключ обязательно является строкой, поскольку он выражен как часть имени операции, но среды выполнения могут разбирать его как числовой литерал при компоновке с коллекцией, использующей числовые индексы.
  • INVOKEDYNAMIC "dyn:getElem"(Object,Object)Object используется для получения элемента объекта-коллекции. Получатель передаётся первым аргументом, ключ элемента — вторым.
  • INVOKEDYNAMIC "dyn:setElem:<key>"(Object,Object)void используется для установки элемента объекта-коллекции (массива, списка, отображения и т. д.). Получатель передаётся первым аргументом, значение — вторым. В этой форме ключ обязательно является строкой, поскольку он выражен как часть имени операции, но среды выполнения могут разбирать его как числовой литерал при компоновке с коллекцией, использующей числовые индексы.
  • INVOKEDYNAMIC "dyn:setElem"(Object,Object,Object)void используется для установки элемента объекта-коллекции (массива, списка, отображения и т. д.). Получатель передаётся первым аргументом, ключ элемента — вторым, значение — третьим.

Операции вызова методов и создания объектов:

  • INVOKEDYNAMIC "dyn:getMethod:<name>"(Object)Object используется для получения именованного метода объекта.
  • INVOKEDYNAMIC "dyn:call"(Object, Object...)Object используется для вызова вызываемого объекта (например, полученного ранее вызовом dyn:getMethod). Первый аргумент — вызываемый объект, необязательные остальные аргументы передаются ему. В зависимости от обстоятельств вторым аргументом операции часто будет объект «this».
  • INVOKEDYNAMIC "dyn:callMethod:<name>"(Object, Object...)Object используется для вызова именованного метода объекта. Первый аргумент — получатель, у которого есть этот именованный метод, необязательные остальные аргументы передаются в вызов. Эту операцию можно реализовать, объединив dyn:getMethod с dyn:call. Отдельные операции поиска и вызова всё равно нужны, поскольку семантика некоторых языков требует, чтобы это были отдельные шаги.
  • INVOKEDYNAMIC "dyn:new"(Object, Object...)Object используется для вызова переданного вызываемого объекта как конструктора. Первый аргумент — объект-конструктор, необязательные остальные аргументы передаются ему. Некоторые языки позволяют вызывать вызываемые объекты и как обычные функции, и как конструкторы, поэтому нужна операция, отдельная от dyn:call.

Механизм компоновки

Точка входа в систему, как обычно для INVOKEDYNAMIC, — bootstrap-метод. У bootstrap-метода должен быть доступ к DYNAMIC LINKER. Bootstrap-метод создаёт экземпляр RELINKABLE CALL SITE и поручает своему DYNAMIC LINKER инициализировать его.

DYNAMIC LINKER — объект, который в конечном счёте координирует компоновку. Инициализируя RELINKABLE CALL SITE, DYNAMIC LINKER устанавливает в качестве цели точки вызова дескриптор своего метода «relink». Этот метод содержит сам алгоритм перекомпоновки, который будет запущен при следующем за этим первом вызове в этой точке вызова.

При вызове метод relink создаёт LINK REQUEST — объект, который содержит имя и сигнатуру точки вызова, а также фактические аргументы вызова, запустившего компоновку. Поскольку в нём есть фактические аргументы вызова, компоновщик получает больше информации, чем доступно bootstrap-методу. LINK REQUEST передаётся компоновщикам GUARDING DYNAMIC LINKER, которыми управляет DYNAMIC LINKER.

GUARDING DYNAMIC LINKER — объект, который может выполнить компоновку для определённого класса объектов (например, для всех объектов, созданных для одного и того же JVM Class). Компоновка, которую создаёт GUARDING DYNAMIC LINKER, обычно условная: она защищена логическим предикатом, ограничивающим её действие (например, «пока получатель является экземпляром List.class» или «пока класс получателя является классом массива» и т. д.). GUARDING DYNAMIC LINKER, который может обработать текущий LINK REQUEST, создаёт GUARDED INVOCATION — тройку из дескриптора метода, реализующего операцию, дескриптора метода, реализующего защитное условие, и, необязательно, набора точек переключения (switch points) для асинхронной инвалидации компоновки.

Когда среда выполнения языка создаёт экземпляр DYNAMIC LINKER, который она будет использовать в bootstrap-методах своих классов, она передаёт ему экземпляр собственного GUARDING DYNAMIC LINKER как один из очевидных компоновщиков, которыми он должен управлять. (Или передаёт несколько компоновщиков, поскольку у языка их может быть больше одного: он может разбить функциональность компоновки на несколько классов; в Nashorn сейчас их восемь.) DYNAMIC LINKER берёт переданные компоновщики, обычно добавляет «beans-компоновщик» как запасной вариант для компоновки обычных Java-объектов, а также создаёт и добавляет компоновщики других языков, которые могут быть видимы через загрузчик классов среды выполнения языка с помощью механизма java.util.ServiceLoader.

DYNAMIC LINKER берёт GUARDED INVOCATION, созданный одним из опрошенных им GUARDING DYNAMIC LINKER, и передаёт его в RELINKABLE CALL SITE. Предполагается, что классы точек вызова, которые могут участвовать в такой высокоуровневой компоновке, реализуют интерфейс RelinkableCallSite, определяющий метод «relink». Через этот метод они получают GUARDED INVOCATION и включают их в свою текущую компоновку.

DYNAMIC LINKER обязан выступать координатором между GUARDING DYNAMIC LINKER, которыми он управляет, и RELINKABLE CALL SITE, которые нужно (пере)компоновать. Этот дизайн разделяет вопрос «что» компонуется, который решает компоновщик (обычно компоновщик языка получателя), и вопрос «как», за который отвечает реализация точки вызова, определяемая средой выполнения языка вызывающей стороны).

Перекомпонуемые точки вызова

Простейшая такая точка вызова — мономорфная RELINKABLE CALL SITE. При каждом вызове relink она отбрасывает текущую компоновку и создаёт комбинатор MethodHandles.guardWithTest(), используя guard-дескриптор переданного GUARDED INVOCATION как дескриптор «test», его дескриптор вызова как дескриптор «target», а дескриптор метода, указывающий обратно на метод relink у DYNAMIC LINKER, как дескриптор «fallback». Так при каждом вызове, если аргументы не проходят guard (то есть текущий скомпонованный вызов для них не подходит), точка вызова будет заново скомпонована для этих аргументов. (Наконец, если GUARDED INVOCATION также несёт ненулевой Switch point, мономорфная точка вызова дополнительно объединит полученный комбинатор с комбинатором инвалидации SwitchPoint.guardWithTest(), снова переходя к перекомпоновке, когда он будет инвалидирован.)

Разумеется, существуют и более сложные классы точек вызова, например цепочечная точка вызова, в которую в любой момент может быть скомпоновано несколько разных методов за счёт построения каскадной цепочки комбинаторов guardWithTest. Её можно дополнительно улучшить: хранить в ней данные профилирования и периодически перекомпоновывать её с новой цепочкой, в которой вызовы упорядочены от наиболее частых к наименее частым, и т. д.

Преобразования значений

Ещё один аспект этой инфраструктуры, который мы не упоминали ранее, но который критически важен для любого динамического языка, — поддержка преобразований значений. API java.lang.invoke уже поддерживает «преобразования при вызове метода» (method invocation conversions), описанные в Java Language Specification, как часть MethodHandle.asType(), но нам нужно подготовиться к языкам, допускающим дополнительные неявные преобразования, например из int в java.lang.String. Если точка вызова скомпонована с дескриптором метода, который ожидает параметр типа String, а в точке вызова этот параметр имеет тип «int» (или, что ещё вероятнее, в байт-коде он имеет тип «Object», но при вызове фактическим значением параметра может оказаться Integer), компоновщику придётся вставить специфичное для языка преобразование типа с помощью MethodHandles.filterArguments().

Подобно DYNAMIC LINKER, в системе должна быть TYPE CONVERTER FACTORY, объединяющая набор GUARDING TYPE CONVERTER FACTORIES. Подобно GUARDING DYNAMIC LINKER, GUARDING TYPE CONVERTER FACTORY создаёт защищённый дескриптор метода, который может преобразовывать значения между заданными исходным и целевым типами. Дескриптор метода должен сопровождаться guard, потому что у точек вызова большинство параметров часто будут иметь тип «Object», и поэтому запрашиваемые преобразователи будут вида «Object в String», «Object в int» и т. д. Среда выполнения языка должна будет предоставить и дескриптор метода-преобразователя, и guard, определяющий, применим ли он. Например, дескриптор метода будет соответствовать утверждению «вот как я в общем случае преобразую Object в int для объектов, которые я распознаю», а guard будет отвечать на вопрос «относится ли фактический объект-аргумент к типу, который я распознаю?».

Последний элемент головоломки, обычно нужный только для выбора среди перегруженных Java-методов, — CONVERSION COMPARATOR. Поскольку мы ввели преобразования, специфичные для языка, при компоновке с методом Java-класса мы можем получить более широкий набор применимых методов, чем допускает язык Java, ведь новые преобразования могут сделать применимыми больше методов. Если бы мы пытались выбирать из этого расширенного набора применимых перегрузок, пользуясь только правилами разрешения JLS, мы бы часто получали ошибку неоднозначности. Поэтому введение новых преобразований типов порождает и потребность в новых правилах ранжирования преобразований. GUARDING TYPE CONVERTER FACTORY может дополнительно реализовать интерфейс CONVERSION COMPARATOR, к которому будет обращаться логика динамического разрешения перегруженных методов. Она передаёт ему исходный тип параметра в точке вызова T и целевые типы параметров в той же позиции у сравниваемых методов-кандидатов U1 и U2, а он должен решить, какое преобразование предпочтительнее: T-в-U1 или T-в-U2.

Разработчикам реализаций языков нужно будет реализовать GUARDING DYNAMIC LINKER для своего языка, при необходимости GUARDING TYPE CONVERTER FACTORY, если их язык допускает больше неявных преобразований, чем JLS, и, наконец, CONVERSION COMPARATOR — в большинстве случаев, когда они также предоставляют GUARDING TYPE CONVERTER FACTORY. Вообще-то нам стоит изучить, не сделать ли функциональность CONVERSION COMPARATOR просто обязательной частью GUARDING TYPE CONVERTER FACTORY.

Неудачная компоновка

Если ни одному GUARDING DYNAMIC LINKER не удаётся скомпоновать операцию, DYNAMIC LINKER выбросит исключение типа NoSuchDynamicMethodException, подкласса RuntimeException. Если GUARDING DYNAMIC LINKER может однозначно определить, что операция завершится неудачей при вызове с аргументами, для которых она компонуется, он может либо сам сразу выбросить специфичное для языка исключение, либо создать GUARDED INVOCATION, который выбрасывает исключение, когда аргументы проходят guard.

Компоновщик Beans

Библиотека содержит предопределённый GUARDING DYNAMIC LINKER с именем BeansLinker, который реализует все поддерживаемые операции над обычными Java-объектами (свойства отображаются либо на методы getter/setter, либо на публичные поля; Java-типы (в отличие от объектов Class) предоставляются как объекты, которые можно использовать как конструкторы и как владельцы статических полей и методов; массивы, списки и отображения ведут себя как коллекции).

Альтернативы

Эту задачу можно было бы решить и с помощью внедрения интерфейсов (interface injection), но сейчас такой возможности в поставляемой JVM нет. В этом случае вместо инструкций INVOKEDYNAMIC код содержал бы инструкции INVOKEINTERFACE, нацеленные на предопределённые интерфейсы, в которых объявлены методы, реализующие эти операции. Наш подход более универсален: мы не ограничиваем поведение компоновки только типом получателя (хотя обычно так и бывает), и наш подход позволяет легко и со слабой связностью комбинировать поведение компоновщиков. Например, узел DOM можно скомпоновать с помощью комбинации компоновщика DOM и обычного Java-компоновщика: компоновщик узлов DOM отображает операции на семантику XML InfoSet, а для операций, не относящихся к DOM, переходит к обычной Java-компоновке.

Интересно также рассмотреть систему, которой INVOKEDYNAMIC вообще не нужен, но которая поддерживает адаптивную перекомпиляцию по требованию, перестраивающую код под разные виды объектов, встречающихся в точках вызова. Опять же, такой системы в JVM сегодня нет, а INVOKEDYNAMIC был специально спроектирован как альтернатива подобным системам.

Тестирование

Nashorn уже использует внутреннюю версию этой библиотеки, и предполагается, что он перейдёт на предлагаемую этим JEP версию (см. раздел «Зависимости»). Библиотека хорошо проверена тестами Nashorn. В исходной внешней версии библиотеки (размещённой на GitHub) есть дополнительные модульные тесты с довольно хорошим покрытием, которые не были перенесены при переводе библиотеки внутрь пакета jdk.internal.dynalink.*. Эти тесты можно было бы взять.

Риски и допущения

Библиотека задумана как полезная для самых разных реализаций языков на JVM. Всегда возможно, что какие-то требования какого-то языка не будут учтены, если мы не получим достаточно отзывов; с другой стороны, важно также проверять каждое такое дополнительное требование на предмет возможного разрастания объёма работ.

Поскольку библиотека предоставляет динамический доступ к методам классов, она потенциально может быть привлекательной целью для атак. Мы хорошо осознаём эти аспекты и потратили много времени на то, чтобы продумать и спроектировать её так, чтобы исключить любое повышение уровня доступа или других привилегий. Иначе говоря, насколько нам известно, она не содержит ошибок безопасности, но потенциально может их содержать, поскольку это привилегированный код, поставляемый в составе JRE, который получает дескрипторы методов и затем выдаёт их.

При этом мы ограничиваем её работу только публичными членами публичных классов в экспортируемых пакетах. Встроенный компоновщик Beans находит дескрипторы методов с помощью MethodHandles.publicLookup(), затем из соображений производительности кэширует их внутри и использует повторно, за исключением вызовов методов, помеченных как чувствительные к вызывающей стороне (caller sensitive): их всегда ищут с помощью MethodHandles.Lookup, переданного в bootstrap-метод, и никогда не кэшируют.

Зависимости

Предполагается, что Nashorn JavaScript Engine (JEP 174) будет перенесён на основу этого JEP, поскольку пакет jdk.internal.dynalink из JDK 8 будет выведен из употребления в JDK 9. Ожидается, что изменение будет незначительным, так как мы будем стремиться в разумных пределах свести к минимуму отличия от существующего API.