Жизнь и удивительные приключения в экзотических JavaScript окружениях
- понедельник, 29 мая 2017 г. в 03:15:02
Вам когда-нибудь приходилось писать на хорошо знакомом языке под никогда ранее невиданную платформу? Странное ощущение.
Кодабра делится опытом, как максимально быстро разобраться с незнакомым окружением и начать жить.
Все знают Minecraft — кубический феномен, за считанное время выросший из инди-проекта никому не известного шведского программиста в одну из главных франшиз самой Microsoft.
Мы в Кодабре очень любим Minecraft, потому что его любят дети, а значит мы можем применять его в учебных целях, используя хорошо знакомый игровой мир, встроенные механики и привычную терминологию, открывая с их помощью удивительный мир программирования.
Проект MinecraftEdu специально создан для объединения процесса игры и обучения, привнося в стандартную игру новые объекты и инструменты, например — программируемых черепашек-роботов, с помощью которых удобно объяснять введение в программирование и алгоритмы.
Хотя "под капотом" у черепашек настоящая Lua, но внешний API слишком ограничен и фактически не подходит ни для чего более сложного, чем автоматизация черепашки. Это быстро наскучивает, а после освоения азов, детям хочется двигаться дальше и делать что-то более сложное и интересное.
Так мы пришли к мысли попробовать использовать JavaScript из мода ScriptCraft, предлагающий полный доступ к созданию своих модов. Язык имеет достаточно низкий порог вхождения и без лишних проблем воспринимается детьми. Труднее приходится взрослым преподавателям, которые до этого работали с Node.js или программировали под веб-браузер, так как ScriptCraft предоставляет не совсем привычную JavaScript-среду, с которой необходимо разобраться до того, как садиться писать курсы и нырять с головой в пучину разработки модов.
Мы обобщили свой опыт начала общения с ScriptCraft так, чтобы его можно было применить к любой другой незнакомой или неизвестной платформе с JavaScript на борту, под которую вам может потребоваться писать код.
Успешно установленный мод добавляет в игру две новые команды — /js [code]
для выполнения JavaScript кода из консоли и /jsp [command]
для выполнения команд, заданных в плагинах через функцию command()
("jsp" здесь не имеет ничего общего с JavaServer Pages).
Давайте протестируем работу мода, введя предложенную в документации команду /js 1 + 1
. Результат действительно будет 2
, но это еще не доказывает почти ничего. Нельзя даже понять, JavaScript ли это выполнился на самом деле? Попробуем убедиться, выполнив что-нибудь более характерное для JS, например, самовызывающуюся функцию, возвращающую ответ на главный вопрос вселенной через специфичное приведение типов.
/js (function () { var a = 4, b = 2; return 'The answer is ' + a + b; }())
Теперь нет никаких сомнений, это действительно JavaScript, но какой именно? Этот вопрос прозвучит странно для большинства программистов на других языках, но, например, опытные фронтенд-разработчики хорошо знают, что JS JS-у рознь и первое, что надо сделать начиная писать код — разобраться с каким именно движком и окружением имеешь дело.
Обычно, самый простой способ узнать, какой движок используется в том или ином продукте, это прочитать документацию либо исходный код к нему. Но в реальном мире это не всегда возможно — документация может отсутствовать или просто опустить такой момент, а исходный код может быть закрыт или недоступен для быстрого понимания. В таких случаях все придется выяснять вручную.
К нашему счастью, мод написан на Java и доступен свободно на Github, так что можно найти, где и как движок подключается — https://git.io/vHc3g.
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");
Если вы не знакомы с Java, то понятнее из этого кода мало что станет. Но мы знаем, что здесь подключается используемый по умолчанию движок JavaScript, которым в Java является либо Rhino (в старых JDK), либо Nashorn (c Java 8). В принципе, на этом можно было бы остановиться и далее идти в Oracle зачитываться формальной документаций, но делать мы этого конечно не будем :) Во-первых это скучно, а во-вторых это нам не даст полного понимания окружения, так как мы находимся внутри Minecraft-мода, написанного взрослым ирландским мужиком, и ожидать тут можно чего угодно.
Но давайте пока предположим, что исходников у нас не было и технологию в основе мы тоже не знаем, как быть тогда?
Немного теории — JavaScript это прежде всего реализация стандарта ECMAScript той или иной версии и любой движок обязан реализовать его полностью или частично. На практике этого мало и, в зависимости от задач производителя, к движку добавляются специфичные расширения и интерфейсы. Назовем это "особенностями реализации" — именно по ним можно отличить один движок от другого.
Например, одной из особенностей Nashorn, является наличие у объектов метода __noSuchProperty__
, используемого для описания поведения при попытке чтения отсутствующего свойства объекта. В частности, этот метод всегда есть у глобального объекта и может служить хорошим косвенным признаком движка.
/js __noSuchProperty__
function __noSuchProperty__() { [native code] }
Для других движков особенности будут своими, к счастью выбор вариантов не так уж велик согласно Википедии — List of ECMAScript engines, а встречающихся в диких условиях движков и того меньше. Обычно выбор всегда состоит из 2-3 вариантов, не больше.
Еще один достаточно эффективный способ узнать больше о движке — бросить эксепшен.
/js throw 'dusk'
Из вывода уже понятно, что мы в Java, а посмотрев полный Java-эксепшен, станет понятно и все остальное.
javax.script.ScriptException: javax.script.ScriptException: 1 in <eval> at line number 1 at column number 0 in <eval> at line number 638 at column number 8
at jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(NashornScriptEngine.java:467)
at jdk.nashorn.api.scripting.NashornScriptEngine.invokeImpl(NashornScriptEngine.java:389)
at jdk.nashorn.api.scripting.NashornScriptEngine.invokeFunction(NashornScriptEngine.java:190)
...
Хорошо, теперь мы сразу несколькими разными способами выяснили, кто именно выполняет наш JavaScript-код и можно двигаться дальше, прямиком в темный мир runtime-окружения.
Для начала давайте проверим, не находимся ли мы в "sctrict mode", так как это может быть важно для техник, с помощью которых мы будем познавать мир.
/js (function () { return !this; }())
false
В строгом режиме this внутри анонимной функции определен не будет. Нам повезло, мы не там, и это значительно развязывает руки.
Далее нужно обязательно найти глобальный объект. Если повезет, this уже окажется им, что легко проверить следующим способом.
/js this === new Function('return this')()
true
Трюк с конструктором Function
универсальный и может быть использован для получения глобального объекта даже в случае, если его имя неизвестно и прямое обращение невозможно.
Нам снова повезло и мы будем использовать this для удобства записи, а так же global в остальных случаях (так как именно global является глобальным объектом в Nashorn, чего мы не знали бы, не выясни мы сначала движок).
Наконец-то пришло время заглянуть в самое сокровенное любого рантайма — в глобальный объект. Помимо отличного способа снять "отпечатки", это даст нам базовые знания о том, что здесь доступно в распоряжение.
Воспользуемся методом Object.getOwnPropertyNames, который возвращает массив всех имен свойств объекта, причем вне зависимости, являются ли они перечислимыми (enumerable) или нет. Этот метод доступен начиная со стандарта ECMAScript 5.1, который и реализован в Nashorn. В случае, если бы этот метод был недоступен, пришлось бы использовать стандартный for..in
и довольствоваться только перечислимыми свойствами.
Объект console в Nashorn не реализован нативно, поэтому я буду использовать метод print()
для вывода информации в консоль сервера, так как это немного удобнее, чем использовать echo()
из мода для вывода прямо в консоль игры.
Выводим нативные методы.
/js print('Native methods:\n' + Object.getOwnPropertyNames(this).filter(function (name) { return (typeof global[name] === 'function' && global[name].toString().indexOf('native code') >= 0) }).join(', '))
Native methods:
parseInt, parseFloat, isNaN, isFinite, encodeURI, encodeURIComponent, decodeURI, decodeURIComponent, escape, unescape, print, load, loadWithNewGlobal, exit, quit, eval, Object, Function, Array, String, Boolean, Number, Error, ReferenceError, SyntaxError, TypeError, Date, RegExp, JSAdapter, EvalError, RangeError, URIError, ArrayBuffer, DataView, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, JavaImporter, __noSuchProperty__
Отдельно — пользовательские методы.
/js print('User-defined methods:\n' + Object.getOwnPropertyNames(this).filter(function (name) { return (typeof global[name] === 'function' && global[name].toString().indexOf('native code') < 0) }).join(', '))
User-defined methods:
__scboot, __onDisable, __onEnable, __onDisableImpl, addUnloadHandler, refresh, echo, alert, scload, scsave, scloadJSON, isOp, require, setTimeout, clearTimeout, setInterval, clearInterval, persist, command, __onTabComplete, plugin, __onCommand, box, box0, boxa, arc, bed, blocktype, copy, paste, cylinder0, cylinder, door, door_iron, door2, door2_iron, firework, garden, ladder, chkpt, move, turn, right, left, fwd, back, up, down, prism0, prism, rand, sign, signpost, wallsign, sphere, sphere0, hemisphere, hemisphere0, stairs, oak, birch, jungle, spruce, commando, castle, chessboard, cottage_road, cottage, dancefloor, fort, hangtorch, lcdclock, logojs, logojscube, maze, rainbow, wireblock, wire, torchblock, repeaterblock, wirestraight, redstoneroad, spawn, spiral_stairs, temple, Drone, hello
Если отделить нативные методы от пользовательских достаточно просто, то со свойствами глобального объекта такой номер уже не проделать. Можно разделить их на основе дескриптора (получив его с помощью Object.getOwnPropertyDescriptor) и флагов configurable / enumerable, но на мой взгляд это не слишком эффективно и гораздо проще выполнить ту же задачу глазами.
/js print('Properties:\n' + Object.getOwnPropertyNames(this).filter(function (name) { return typeof global[name] !== 'function' }).join(', '))
Properties:
arguments, NaN, Infinity, undefined, Math, Packages, com, edu, java, javafx, javax, org, __FILE__, __DIR__, __LINE__, JSON, Java, javax.script.filename, global, server, nashorn, config, __plugin, console, events, arrows, classroom, blocks, entities, homes, Game_NumberGuess, signs, self, __engine
Так как по результатам вывода методов мы уже понимаем, что большая часть API плагинов экспортируются в глобальную зону видимости, то можем легко разделить более или менее стандартные для любого JS окружения глобальные свойства arguments, NaN, Infinity, undefined, Math, JSON и специфичные для Nashorn Packages, com, edu, java, javafx, javax, org, __FILE__, __DIR__, __LINE__, javax.script.filename, classroom, а все, что останется, скорее всего было добавлено самим модом ScriptCraft.
Что еще нам говорит вывод глобальных переменных? Набор нативных методов и свойств в принципе достаточно стандартен для ES 5.1 окружения. Привычных API из Node.js и тем более из браузера здесь конечно нет, зато в изобилии обертки для доступа к внутреннему миру Java. Сам мод без особых терзаний совести экспортирует как внешние так и внутренние интерфейсы в глобальный объект с произвольными именами, переопределяет некоторые нативные методы, такие как setTimeout()
, setInterval()
, clearTimeout()
, clearInterval()
, добавляет объект console и метод require()
, работающий в стиле Node.js.
Вы можете использовать метод toString()
у функции для получения строкового представления тела функции (кроме нативных), а так же свойства name и length для получения имени (полезно для функций за bind'ом) и количества принимаемых аргументов соответственно (полезно для неизвестных нативных функций).
Хотя полученной информации уже достаточно для начала работы, наше микро-исследование было бы неполным без определения своего места в цепочке вызовов (call stack), это знание позволит упростить будущую отладку.
В JavaScript проще всего получить stack trace из произвольного места не останавливая выполнение кода, путем создания нового экземпляра объекта Error и обращения к его динамически-генерируемому свойству stack.
Выполнение команды /js print(new Error().stack)
недвусмысленно говорит нам, что мы находимся в некотором REPL интерфейсе и все, что мы введем, попадет в ту или иную форму eval в движке. Оно и логично, все же мы пытаемся исполнять код из консоли.
Error
at <program> (<eval>:1)
at __onEnable$__onCommand (<eval>:613)
Примечательно, что __onEnable$__onCommand
здесь скорее всего сгенирированный Java-класс.
Выполнение того же кода из внешнего файла, ситуацию в корне не изменит по-видимому из-за реализации механизма загрузки модулей и особенностей самого движка.
Error
at <anonymous> (<eval>:1)
at <anonymous> (<eval>:278)
at <anonymous> (<eval>:306)
at <anonymous> (<eval>:56)
at __onEnable (<eval>:783)
at <anonymous> (<eval>:91)
Зато теперь мы легко можем узнать точку входа в загрузчик, просто поискав вызов __onEnable в исходниках.
Описанный выше эвристический алгоритм показывает себя хорошо в ситуациях, подобных нашей, когда не очень понятно, с чем вообще придется иметь дело и с какой стороны тут лучше подступиться. Все, что вам на самом деле нужно, чтобы быстро вникнуть в суть дел — поле, в которое можно вводить код. Подход несколько экстремальный, но зато весьма эффективный и может работать даже при полном отсутствии документации и инструментов отладки, правда в этом случае понадобится значительно больше трюков.
Стоит еще скать, что в этой статье мы никак не затронули API Nashron для доступа к Java, потому как напрямую к теме статьи это не относится и там можно погрязнуть надолго. Если коротко, весь Nashorn устроен таким образом, чтобы Java-программисты ничем не были ограничены при работе из JavaScript. Доступ практически неограничен – можно распараллеливать выполнение кода, создавая родные для Java треды, можно наследоваться от Java-классов, можно создавать несколько глобальных объектов, динамически подгружать код, читать файлы, выполнять команды ОС и многое, многое другое.
Вот лишь несколько ссылок для интересующихся: