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

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



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


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


    Кодабра
    0,00
    Учим детей программировать игры
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      Спасибо. Правда, в «А теперь за работу!» я ожидал увидеть написание собстенно мода, а то так и осталось неосвященным что можно такого сделать с игрой посредством js.
        0

        Заголовок и впрямь неудачный, подразумевалось, что работать все начнут самостоятельно :)
        Вот вам кстати отличный официальный гайд по ScriptCraft, который очень хорошо описывает что и как там можно делать https://github.com/walterhiggins/ScriptCraft/blob/master/docs/YoungPersonsGuideToProgrammingMinecraft.md#the-young-persons-guide-to-programming-in-minecraft

          0
          Прошу, не сочтите за рекламу.

          Все плагины описанные на этой странице разработаны именно на scriptcraft-e
          https://lastuniverse.ru/плагины

          По завершении разработки исходники будут представлены на github-е

          Если кого заинтересует — в личку, скину архивом.
        –1
        ЛОЛ! а как же OpenComputers?
          +3
          Ну если говорить про ComputerCraft и его клон OpenComputers — то у них есть один фатальный недостаток одним махом исключающий их из этой статьи. У них в качестве ЯП выбран LUA.
            –1
            АХАХАХ OpenComputers клон ComputerCraft, что за бред! вы бы хотя-бы поинтересовались сначала, прежде
            чем говорить!
            P.S чем вам lua не угодил?
              0
              OC был создан позже чем CC, работает на тех же механиках что и СС, ЯП тот же что и в СС, интерфейсы теже что и в СС. Если что его и отличает — это стоимость механизмов. И право называть его клоном или нет, глядя на всё это, я оставлю за собой.
              А кто сказал что LUA должен мне угождать или не угождать? Эта тема о JavaScript окружениях не?
                0
                Механика другая, ЯП тотже, интерфейсы другие, ладно не буду спорить с человеком который говорит о том о чём не знает.
                  0
                  Понимаете ли вы вообще что значит механика и интерфейсы? И там и там всё почти что одинаковое, разве что API отличается. Какое-то фантастическое неофитство.
              0
              И кстати MinecraftEdu тоже на Lua, но в статье есть.
              К тому же ни один из этих модов даже рядом не стоит с OpenComputers!
                +1
                В данной статье не стоит вопрос о том кто с кем рядом стоит, или не стоит. Тема этой статьи совсем о другом.
            +1

            А почему в качестве базы для "кубических песочниц" не использовать MineTest?


            Он совершенно свободный и дорабатывать его проще. Там даже архитектура специально на это расчитана.

              0

              Между прочим Nashorn/Rhino не такие уж и "экзотические". Да, это не браузер — нету window, нету event loop. Но это полноценные JavaScript движки. Никто не мешает даже писать на ES6 и транспилить это дело babel-ом. И даже React там может работать.

                0

                В движках самих по себе что-то экзотическое бывает редко (если не считать баги) – все же они так или иначе должны реализовывать стандарт, иначе это нельзя будет назвать JavaScript-ом. Экзотика всегда заключена в окружении (нестандартные host-объекты, внешние API, расширения языка), в этом смысле Rhino/Nashorn, которые используются в основном для встраивания в Java-приложения, выделяются довольно сильно на фоне общераспространенных v8/JSC/Chakra, но сравнение движков как таковых, целью данной статьи не являлось.

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

              Самое читаемое