GraalVM обеспечивает бесшовную совместимость с полиглотами. Это позволяет движку JavaScript GraalVM (Graal.js) получать доступ к чужим объектам, созданным другим языком программирования, как если бы они были созданы на JavaScript. В нашем недавнем выпуске GraalVM 22.2 мы включили функцию по умолчанию для дальнейшего расширения этой поддержки: чтобы внешние объекты использовали прототипы JavaScript, чтобы внешние объекты вели себя еще больше в соответствии с тем, что ожидает приложение JavaScript.

Введение в Polyglot Interop

Используя Graal.js, вы можете обмениваться данными из других языков программирования со своим приложением JavaScript. Протокол взаимодействия Truffle Polyglot позволяет получить доступ к этим данным с помощью простого кода JavaScript. Рассмотрим следующий пример Java, который создает массив Python и обращается к нему из кода JavaScript:

1: import org.graalvm.polyglot.*;
2: public class Example1 {
3:   public static void main(String argv[]) {
4:     try (Context ctx = Context.create()) {
5:       Value arr = ctx.eval("python", "[1,2,3]");
6:       ctx.getBindings("js").putMember("pArr",arr);
7:       String src = "var s=0; for (var i=0;i<pArr.length;i++) { s+=pArr[i]; }; s";
8:       System.out.println(sum.eval("js", src).asInt()); //6 expected
9: } } }

В строке 4 примера создается объект контекста типа org.graalvm.polyglot.Context. В этом контексте создается массив с использованием кода Python (строка 5), который возвращается в Java как экземпляр класса org.graalvm.polyglot.Value. Затем этот объект сохраняется в глобальном контексте JavaScript в строке 6. В строке 7 выполняется код JavaScript. С помощью цикла for осуществляется доступ к длине массива и его элементам для их суммирования. Результат снова возвращается в Java и печатается.

Как это волшебно работает? Всякий раз, когда операция над внешним массивом выполняется в языковых интерпретаторах GraalVM, выполнение фактически отправляется с использованием сообщений на иностранном языке, на котором был создан объект — метод, установленный ранними объектно-ориентированными языками, такими как Smalltalk. Таким образом, все операции чтения или записи в массив выполняются языком, на котором был создан массив. В нашем примере это Python. Выполнение этих операций на иностранном языке позволяет правильно применять всю семантику этой операции, при этом JavaScript не должен знать о семантике. Когда интерпретатор JavaScript отправляет сообщение взаимодействия getArraySize, интерпретатор GraalVM Python знает, как его интерпретировать и как считывать длину массива Python: вызывая функцию Python len. Интерпретатор Python также знает точную семантику и крайние случаи, связанные с чтением и записью элементов массива. Делегируя доступ к Python, JavaScript не нужно беспокоиться о защищенных элементах или доступе за пределами границ — внешний интерпретатор (Python) позаботится об этом и будет действовать соответствующим образом. Это хорошо работает для всех основных операций, которые обычно выполняются с объектами, массивами и т.п.

Встроенные методы не работают — прототипы вызывают проблемы

Но как насчет всего библиотечного кода, предоставляемого языком? В JavaScript есть встроенные методы для работы с массивами, такие как indexOf, sort или map. Можно ли вызывать эти методы для посторонних объектов?

Давайте посмотрим на спецификацию JavaScript (ECMAScript). Многие из методов, предоставляемых основной библиотекой JavaScript, на самом деле предназначены для работы со всеми типами объектов. Например, большинство методов в Array.prototype будут работать не только с массивами, но и с обычными объектами — до тех пор, пока объект ведет себя как массив. Он должен предоставить свойство length и предоставить свойства, имена которых представляют индексы массива в диапазоне от 0 до length-1. ». Если эти требования соблюдены, вы можете вызывать методы Array.prototype для этого объекта. Например, вы можете вызвать метод indexOf для такого объекта, и он будет работать так, как ожидалось:

//finds element with value "42" on the element with index 1
Array.prototype.indexOf.call( { length: 2, "0": 0, "1": 42}, 42) === 1

Конечно, использование этого шаблона неудобно каждый раз вводить. И самое главное, это не будет работать в общем случае, когда библиотека или какой-то пользовательский код действительно ожидает, что объект-приемник будет определенного типа. В конце концов, для массивов вы можете вызывать myArray.indexOf(42) напрямую, и это то, что библиотеки и пользовательский код используют в своих реализациях. Однако на простых объектах вы не можете вызвать метод indexOf — у объектов его просто нет!

Для посторонних объектов проблема очень похожа на различие между массивом и объектом. Пока внешний объект может предоставлять длину и может читать и в некоторых случаях записывать свои элементы, он, вероятно, может использоваться методами массива JavaScript. Однако, как и в случае с показанными выше простыми объектами, это будет работать только в том случае, если метод применяется явно с помощью метода call. Как и ожидало большинство программистов, т. е. простой pythonArray.indexOf(42) потерпит неудачу — здесь pythonArray ведет себя аналогично обычному объекту JavaScript: он может обеспечить необходимую функциональность. , но у него нет ожидаемых методов.

Чтобы усложнить ситуацию, обратите внимание, что внешний объект по-прежнему будет иметь методы, предоставляемые его собственным языком. В Python может быть метод indexOf для массивов, но он может иметь другие аргументы или даже вести себя совершенно иначе. И хотя протокол взаимодействия GraalVM способен выполнять такую ​​постороннюю функцию, если она существует, это, вероятно, не то, что вы хотите делать. Поскольку вы работаете с приложением JavaScript, скорее всего, вы хотите, чтобы TYPE.prototype.METHOD выполнялся, соблюдали его точную семантику и отлавливали любые сбои, которые он может вызвать.

Использование прототипов для решения проблемы

Как мы уже видели, простой обходной путь — явный вызов метода, который мы хотим выполнить:

Array.prototype.indexOf.call(pythonArr, 3);

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

Здесь вам поможет двигатель. Очевидно, что все интересующие нас методы, скажем, для массивов определены в одном месте: Array.prototype. Это предлагает простое решение нашей проблемы: назначьте Array.prototype в качестве прототипа для каждого внешнего массива, как мы это делаем с массивами JavaScript. Поскольку это автоматически выполняется движком, вы можете вызывать все доступные методы Array.prototype — даже те, которые вы или библиотека предоставили — для внешних массивов.

Этот режим был доступен в нескольких релизах и сейчас находится под экспериментальным флагом. Поскольку в целом мы получили положительные отзывы об этой функции, мы включили ее по умолчанию в выпуске GraalVM 22.2. Пользователи по-прежнему могут отказаться от такого поведения, явно установив для флага js.foreign-object-prototype значение false.

Ограничения этого подхода

Будет ли это работать для каждого массива и каждого метода? У такого подхода есть очевидные ограничения. Например, такие операции, как Array.prototype.unshift или Array.prototype.push, могут работать только с массивами, длина которых может увеличиваться. Хотя наше решение в целом работает с массивами Java, вы не можете переместить или отменить сдвиг в массив Java, поскольку они имеют фиксированный размер и не могут увеличиваться. Java выдаст исключение, если вы попытаетесь это сделать.

Другая опасность всегда присваивать прототип JavaScript внешним объектам заключается в том, что они могут этого просто не ожидать. Предлагаемый подход отлично работает для существующего (JavaScript) кода, который внезапно видит новый внешний тип и должен обрабатывать его наилучшим образом. Если вы пишете новое полиглот-приложение с нуля, вам может понадобиться больше гибкости в том, что вы вызываете: иногда сторонний метод может иметь больше смысла, чем метод JavaScript. Это проблема, которую фреймворк не может решить за вас автоматически.

Приоритет JavaScript по сравнению с иностранным

Сложный вопрос, который нужно решить, заключается в том, какой метод должен иметь приоритет, когда и соответствующий прототип JavaScript, и внешний объект предоставляют соответствующий метод. Рассмотрим метод toString в примере, когда объект Java передается в JavaScript. Теперь, когда вы выполняете theObject.toString, явно или неявно, должен ли выполняться java.lang.Object.toString Java или Object.prototype.toString? Должен ли этот вопрос зависеть от того, перезаписываете ли вы явно один из этих методов?

Ответ JavaScript на это прост: сначала проверьте сам объект («собственное свойство»). Только если вы не можете найти его на объекте, поднимайтесь по цепочке прототипов и ищите там. Для посторонних объектов эта семантика требует, чтобы мы уже запрашивали протокол взаимодействия на самом первом шаге — поэтому, если посторонний объект может предоставить подходящий метод с таким именем, мы вызываем его. Протокол взаимодействия, не зависящий от языка, не понимает «прототипов» и поэтому использует подход «наилучшие усилия» для предоставления запрошенного значения. В случае с Java он предоставляет не только реализации самого объекта, но и его суперклассы.

Это то, чего мы хотим? Есть аргументы за и против такого поведения. Примером правильного поведения являются прокси-серверы на нашем хост-языке Java. Возможно, вы захотите предоставить реализацию org.graalvm.polyglot.proxy.ProxyObject или org.graalvm.polyglot.proxy.ProxyExecutable, чтобы вы могли перехватывать и манипулировать поведением вашего проксируемый объект. Однако для этого требуется, чтобы этот прокси всегда вызывался ДО собственной реализации в JavaScript — в противном случае прокси будет бесполезен в тех случаях, когда существует реализация TYPE.prototype.METHOD. Здесь должна победить собственная собственность.

Простой встречный пример — это случай, когда и иностранный язык, и JavaScript предоставляют определенный метод:

Value arr = ctx.eval("ruby", "[1,2,3]");
ctx.getBindings("js").putMember("rubyArr",arr);
Value sum = ctx.eval("js", "rubyArr.map( (v)=> v*v );");

В этом примере вы хотите использовать функцию Array.prototype.map для возведения в квадрат всех значений. Как ни странно, этот пример завершится ошибкой с ArityException. Основываясь на поведении собственного свойства, функция карты Ruby была найдена и выполнена, но она ожидает другой набор аргументов и, следовательно, терпит неудачу. В этом примере ясно, что нам не нужно поведение собственного свойства — мы хотим выполнить Array.prototype.map, а не чужую функцию map, которая может иметь странную поведение.

К сожалению, на данный момент мы не можем решить эту проблему. Решение потребует, чтобы протокол взаимодействия лучше осознавал разницу между собственными и унаследованными свойствами и, таким образом, позволял нам различать эти случаи. На данный момент, начиная с GraalVM 22.2., мы используем решение, согласно которому собственные свойства внешних объектов имеют приоритет — т. е. Java-прокси будут работать, а внешние объекты могут не работать.

Добавление явного прототипа для лучшей конфигурируемости

Чтобы обойти некоторые ограничения, вы можете настроить реализацию некоторых встроенных методов. С подходом, описанным до сих пор, вам нужно будет коснуться, например. Array.prototype для этого. Это представляет некоторую опасность, поскольку вы изменяете код для всех массивов, как внешних, так и собственных. Если вы сделаете это, возникнут потенциальные опасности, связанные с совместимостью и производительностью.

Чтобы избежать некоторых из этих проблем, мы ввели промежуточный прототип. Внешним массивам назначается ForeignArray.prototype, прототипом которого, в свою очередь, является Array.prototype. Помимо прототипа, объект ForeignArray пуст. Хотя все методы по-прежнему можно найти, проходя по цепочке прототипов, это дает пользователям возможность внедрить свои собственные реализации встроенных функций в ForeignArray.prototype, чтобы переопределить то, что Array.prototype предоставит.

Единственный потенциальный недостаток этого решения связан с кодом, который выполняет явную проверку прототипа для Array.prototype с помощью оператора равенства или идентификации — он найдет ForeignArray.prototype и может прийти к выводу, что это не так. массив. Это не влияет на оператор instanceof и метод Array.isArray и работает должным образом, как и ожидалось, поэтому для таких проверок следует отдавать предпочтение. Кажется плохой практикой проверять только непосредственный прототип, а не всю цепочку, но код может сделать это — будьте осторожны.

Поделитесь с нами вашими мыслями

Нам очень интересно услышать от вас, работает ли этот подход для вас. Какое сочетание языков вы используете? Как вы решали задачи? Работает ли этот новый режим по умолчанию для вас, или какие проблемы вы находите с ним?

Это область активной разработки, поэтому сообщите нам, в каком направлении вы хотите видеть Graal.js и GraalVM Polyglot API в будущем и какие функции вы хотите добавить.