Открыть список
Как стать автором
Обновить
0
Рейтинг
Кодабра
Учим детей программировать игры

Жизнь и удивительные приключения в экзотических JavaScript окружениях

Блог компании КодабраНенормальное программированиеJavaScriptJavaРазработка игр

Вам когда-нибудь приходилось писать на хорошо знакомом языке под никогда ранее невиданную платформу? Странное ощущение. Кодабра делится опытом, как максимально быстро разобраться с незнакомым окружением и начать жить.



Minecraft-программирование для детей и взрослых


Все знают Minecraft — кубический феномен, за считанное время выросший из инди-проекта никому не известного шведского программиста в одну из главных франшиз самой Microsoft.


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


Проект MinecraftEdu специально создан для объединения процесса игры и обучения, привнося в стандартную игру новые объекты и инструменты, например — программируемых черепашек-роботов, с помощью которых удобно объяснять введение в программирование и алгоритмы.


Интерфейс программирования черепашки


Хотя "под капотом" у черепашек настоящая Lua, но внешний API слишком ограничен и фактически не подходит ни для чего более сложного, чем автоматизация черепашки. Это быстро наскучивает, а после освоения азов, детям хочется двигаться дальше и делать что-то более сложное и интересное.


Так мы пришли к мысли попробовать использовать JavaScript из мода ScriptCraft, предлагающий полный доступ к созданию своих модов. Язык имеет достаточно низкий порог вхождения и без лишних проблем воспринимается детьми. Труднее приходится взрослым преподавателям, которые до этого работали с Node.js или программировали под веб-браузер, так как ScriptCraft предоставляет не совсем привычную JavaScript-среду, с которой необходимо разобраться до того, как садиться писать курсы и нырять с головой в пучину разработки модов.


Мы обобщили свой опыт начала общения с ScriptCraft так, чтобы его можно было применить к любой другой незнакомой или неизвестной платформе с JavaScript на борту, под которую вам может потребоваться писать код.


/js — куда я попал и что тут можно делать?


Успешно установленный мод добавляет в игру две новые команды — /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; }())


The answer


Теперь нет никаких сомнений, это действительно 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'


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.


setTimeout


Вы можете использовать метод 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-классов, можно создавать несколько глобальных объектов, динамически подгружать код, читать файлы, выполнять команды ОС и многое, многое другое.


Вот лишь несколько ссылок для интересующихся:


Теги:javascriptminecraftjavanashorncodabra
Хабы: Блог компании Кодабра Ненормальное программирование JavaScript Java Разработка игр
Всего голосов 27: ↑27 и ↓0 +27
Просмотры10.8K

Комментарии 15

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Похожие публикации

Лучшие публикации за сутки

Информация

Дата основания
Местоположение
Россия
Сайт
codabra.org
Численность
11–30 человек
Дата регистрации

Блог на Хабре