Я скомпилировал первый в мире движок JS обратно в JS

JavaScript WebAssembly
Я скомпилировал первый в мире движок JS обратно в JS

В 1995 году, когда мне был всего один год, парень по имени Брендан Эйх с другой стороны океана за десять дней создал язык программирования, которым я зарабатываю на жизнь сегодня, это JavaScript.

История быстрого создания JavaScript широко распространена среди программистского сообщества. Но для современных людей, возможно, немногие помнят (или даже испытывают), каким был самый ранний JavaScript, не говоря уже о том, чтобы читать исходный код JS-движка года.

Но в 2020 году у нас есть возможность узнать об этой истории. На научной конференции HOPL-IV по истории языков программирования Брендан Эйх и ведущий автор ES6 Аллен Вирфс-Брок написали в соавторстве «20 лет JavaScript"подробно описывает историю зарождения и эволюции JS. Как переводчик китайской версии этой книги, я отредактировал более 600 справочных ссылок в оригинальной версии одну за другой, и одна из них указывает на самый ранний исходный код движка JS. Это вызвало у меня любопытство — можно ли скомпилировать и запустить самый ранний код движка JS сегодня? Если возможно, может ли он сделать еще один шаг и скомпилировать его обратно в JavaScript, чтобы вернуть его к жизни в Интернете? Так что я сделал эту попытку.

Самый ранний JS-движок назывался Mocha (это кодовое имя для внутреннего проекта языка веб-скриптов Netscape), а первый прототип был завершен Бренданом Эйхом в мае 1995 года. На протяжении 1995 и большей части 1996 года Эйх был единственным штатным разработчиком, работавшим над движком JavaScript. До выпуска Netscape 3.0 в августе 1996 года кодовая база Mocha состояла в основном из кода этого прототипа. Версия JS, выпущенная вместе с Netscape 3.0 и названная JavaScript 1.1, ознаменовала завершение начальных этапов разработки JavaScript. После этого Эйх провел еще две недели, переписывая Mocha, получая более сильный движок, которым сегодня является SpiderMonkey в Firefox.

Если вы погуглите «исходный код Netscape», вы, вероятно, сможете отследить только код движка SpiderMonkey в проекте Mozilla в 1998 году. Настоящий исходный код движка Mocha находится в сжатом пакете исходного кода браузера Netscape 3.0.2 (неизвестный источник) в Интернете. Но исходный код Mocha был полностью заброшен после того, как Eich переписал SpiderMonkey, как его оживить?

На самом деле, если вы хотите понять какое-либо программное обеспечение, средства — это не что иное, как «сверху вниз» и «снизу вверх». Первый начинается с уровня архитектуры, чтобы понять макрознание, а второй начинается с уровня кода, чтобы решать микропроблемы. Поскольку я уже знаком с использованием движков JS, таких как QuickJS, я напрямую выбираю метод практики «снизу вверх». Основная идея проста:Инкрементальная компиляция различных модулей движка и, наконец, их сборка для запуска.

Первоначальный Mocha использовал Makefiles в качестве системы сборки, но очевидно, что он некорректно работает с современными операционными системами — еще в те дни, когда MacOS все еще использовал процессоры PPC! Но, в конце концов, система сборки — это не что иное, как автоматизированнаяgccа такжеclangЭто просто вспомогательный инструмент для компилятора. Вкратце, процесс компиляции проекта на языке C представляет собой не что иное, как следующие вещи:

  1. использоватьgcc -cкоманда, одна за другой будет "использоваться как библиотека".cисходный код, скомпилированный в.oформатировать объектный файл. Это скомпилирует каждую функцию в исходном коде C в так называемый «символ» в двоичном исполняемом файле, как в модуле ES.exportФункция, которая выходит, такая. Обратите внимание, что в это время каждый объектный файл может быть произвольно вызван для.hAPI других библиотек, импортированных в форму. На данный момент при компиляции нет ошибок, в объектном файле регистрируются только обращения к внешним символам.
  2. использоватьarзаказать эти.oобъектный файл, созданный.aСтатическая библиотека формата. На самом деле это эквивалентно простому склеиванию и сборке файлов..aФайл будет содержать все символы в проекте, что-то вродеcat *.js >> all.jsЭффект. Кроме того, мы также можем создать более компактную динамическую библиотеку, но это относительно сложно и здесь мы пропустим ее.
  3. использоватьgcc -lКоманда компилирует "вызовите эту библиотеку".cисходный код, то компилятор объединит свой продукт с.aссылка на статические библиотеки. Компоновщик соединит символические зависимости в виде «врезной и шиповой структуры» в каждом объектном файле. На этом этапе для каждого объектного файла на первом этапе все символы, которые вызывают внешние API, должны быть найдены компоновщиком, и отсутствие любого символа приведет к сбою ссылки, но пока ссылка успешна, мы получим последующийmainФункция является точкой входа исполняемого файла.

Таким образом, весь процесс постепенной миграции таков, что:

  1. Скомпилируйте каждую внутреннюю копию Mocha (т.е. кроме записи).cисходный файл, получить файл, содержащий его символы.oФорматировать объектный файл.
  2. будет содержать эти символы.oОбъектные файлы соединяются вместе и упаковываются.aФормат файлов статической библиотеки, т.е.libmocha.a.
  3. Скомпилируйте запись Mochamo_shell.cфайл, используйте его сlibmocha.aСтатическая библиотека подключается для получения финального исполняемого файла.

В этом процессе необходимо иметь дело с некоторыми внешними зависимостями, наиболее типичной из которых являетсяprxxx.hзависимость. Это было разработано Netscape тогдаNetscape Portable RuntimeКроссплатформенная стандартная библиотека, в которой реализованы некоторые общие макроопределения и определения типов, а также базовые структуры данных, такие как хэш-таблицы C и связанные списки, а также некоторые математические вычисления, преобразование времени и другие функции. Исходный код NSPR также был включен в исходный код Netscape 3, но я не перенес их все сразу в новую портированную кодовую базу Mocha. Подход здесь заключается в рекурсивном импорте задействованных файлов заголовков NSPR и исходного кода вручную только при обнаружении отсутствующих зависимостей NSPR, тем самым удаляя минимальное пригодное для использования дерево кода Mocha.

Изменения исходного кода, связанные во всем процессе портирования, в основном включают следующее:

  • Удалитьprcpucfg.h, который напрямую использует порядок байтов с прямым порядком байтов x86 и WASM.
  • Исправлятьprtypes.hОпределения типов в стандарте C99uint16_tзаменятьunsigned shortи другие типы с проблемами совместимости и т.п.BoolТипы.
  • ПополнитьMOCHAFILEМакрос, который заставляет Mocha переходить в режим командной строки для чтения файлов вместо встроенного режима, используемого в браузерах.
  • Дополнительная часть кода отсутствуетincludeЦитировать.

В конце концов мне удалось скомпилировать все модули Mocha с помощью очень простого скрипта bash. Я считаю, что если вы изучите язык C в течение нескольких дней, вы сможете понять:

function compile_objs() {
    echo "compiling OBJS..."
    $CC -Iinclude src/mo_array.c -c -o out/mo_array.o
    $CC -Iinclude src/mo_atom.c -c -o out/mo_atom.o
    $CC -Iinclude src/mo_bcode.c -c -o out/mo_bcode.o
    $CC -Iinclude src/mo_bool.c -c -o out/mo_bool.o
    $CC -Iinclude src/mo_cntxt.c -c -o out/mo_cntxt.o
    $CC -Iinclude src/mo_date.c -Wno-dangling-else -c -o out/mo_date.o
    $CC -Iinclude src/mo_emit.c -c -o out/mo_emit.o
    $CC -Iinclude src/mo_fun.c -c -o out/mo_fun.o
    $CC -Iinclude src/mo_math.c -c -o out/mo_math.o
    $CC -Iinclude src/mo_num.c -Wno-non-literal-null-conversion -c -o out/mo_num.o
    $CC -Iinclude src/mo_obj.c -c -o out/mo_obj.o
    $CC -Iinclude src/mo_parse.c -c -o out/mo_parse.o
    $CC -Iinclude src/mo_scan.c -c -o out/mo_scan.o
    $CC -Iinclude src/mo_scope.c -c -o out/mo_scope.o
    $CC -Iinclude src/mo_str.c -Wno-non-literal-null-conversion -c -o out/mo_str.o
    $CC -Iinclude src/mocha.c -c -o out/mocha.o
    $CC -Iinclude src/mochaapi.c -Wno-non-literal-null-conversion -c -o out/mochaapi.o
    $CC -Iinclude src/mochalib.c -c -o out/mochalib.o
    $CC -Iinclude src/prmjtime.c -c -o out/prmjtime.o
    $CC -Iinclude src/prtime.c -c -o out/prtime.o
    $CC -Iinclude src/prarena.c -c -o out/prarena.o
    $CC -Iinclude src/prhash.c -c -o out/prhash.o
    $CC -Iinclude src/prprf.c -c -o out/prprf.o
    $CC -Iinclude src/prdtoa.c \
        -Wno-logical-not-parentheses \
        -Wno-shift-op-parentheses \
        -Wno-parentheses \
        -c -o out/prdtoa.o
    $CC -Iinclude src/log2.c -c -o out/log2.o
    $CC -Iinclude src/longlong.c -c -o out/longlong.o
}

Конечно, в предупреждении компилятора, брошенном посередине, я также увидел код, который не говорит о боевых искусствах. Напримерmo_date.cэтот в:

if (i <= st + 1)
    goto syntax;
for (k = (sizeof(wtb)/sizeof(char*)); --k >= 0;)
    if (date_regionMatches(wtb[k], 0, s, st, i-st, 1)) {
        int action = ttb[k];
        if (action != 0)
            if (action == 1) /* pm */
                if (hour > 12 || hour < 0)
                    goto syntax;
                else
                    hour += 12;
            else if (action <= 13) /* month! */
                if (mon < 0)
                    mon = /*byte*/ (action - 2);
                else
                    goto syntax;
            else
                tzoffset = action - 10000;
        break;
    }
if (k < 0)
goto syntax;

Есть также много заметок, напоминающих мне о долгой истории этого проекта, таких какmocha.cэтот в:

/*
** Mocha virtual machine.
**
** Brendan Eich, 6/20/95
*/

Также я нашел код, отражающий проблему совместимости с Chaos 1995 года. Они помогают мне понять, почему люди в то время ожидали от Java «написать один раз и запустить где угодно»:

#if defined(AIXV3)
#include "os/aix.h"

#elif defined(BSDI)
#include "os/bsdi.h"

#elif defined(HPUX)
#include "os/hpux.h"

#elif defined(IRIX)
#include "os/irix.h"

#elif defined(LINUX)
#include "os/linux.h"

#elif defined(OSF1)
#include "os/osf1.h"

#elif defined(SCO)
#include "os/scoos.h"

#elif defined(SOLARIS)
#include "os/solaris.h"

#elif defined(SUNOS4)
#include "os/sunos.h"

#elif defined(UNIXWARE)
#include "os/unixware.h"

#elif defined(NEC)
#include "os/nec.h"

#elif defined(SONY)
#include "os/sony.h"

#elif defined(NCR)
#include "os/ncr.h"

#elif defined(SNI)
#include "os/reliantunix.h"
#endif

К счастью, код C компилируется без проблем. В целях сохранения исторических реликвий здесь не было сделано никаких лишних изменений. После получения всех объектных файлов просто используйте следующие строки сценария bash, чтобы связать исполняемый файл Mocha!

function compile_native() {
    export CC=clang
    export AR=ar
    compile_objs
    echo "linking..."
    $AR -rcs out/libmocha.a out/*.o
    $CC -Iinclude -Lout -lmocha tests/mo_shell.c -o out/mo_shell
    echo "mocha shell compiled!"
}

После получения родной версии Mocha, как мне получить версию WASM? очень просто, достаточно поставить родной компиляторgcc(В macOS это на самом делеclang) в компилятор WASMemccВот и все! Компилятор Emscripten поддерживает JavaScript и WASM в качестве бэкэнда компиляции, а переключение формата вывода — это всего лишь вопрос изменения параметра компиляции:

function compile_web() {
    export CC=emcc
    export AR=emar
    compile_objs
    echo "linking..."
    $AR -rcs out/libmocha.a out/*.o
    $CC -Iinclude -Lout -lmocha tests/mo_shell.c \
        --shell-file src/shell.html \
        -s NO_EXIT_RUNTIME=0 \
        -s WASM=$1 \
        -O2 \
        -o $2
    echo "mocha shell compiled!"
}

function compile_js() {
    compile_web 0 out/mocha_shell_js.html
}

function compile_wasm() {
    compile_web 1 out/mocha_shell_wasm.html
}

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

$ source build.sh

# build WASM
$ compile_wasm

# build js
$ compile_js

# build native
$ compile_native

Однако продукт компиляции Emscripten по умолчанию очень навязчив, а сам вывод представляет собой HTML, который «выполнит содержимое WASM синхронно, как только откроется страница». Как я могу заставить его принимать пользовательский ввод из текстового поля? Для простоты страница движка WASM встроена непосредственно в iframe. Каждый раз, когда вы нажимаете кнопку «Выполнить» на странице, содержимое поля ввода будет сначала вставлено в localStorage, а затем будет перезагружена соответствующая страница iframe WASM, на которой содержимое строкового JS-скрипта в localStorage будет считываться синхронно с (симулируется Emscripten) stdin Стандартный ввод и, наконец, автоматически запускает выполнение интерпретации Mocha.

Этот процесс очень прост, и я считаю, что любой обычный фронтенд-разработчик легко его реализует. Вот окончательный эффект:

Закончено! Мы переустановили первый в мире движок JS обратно в браузер!

С момента переноса исходного кода Mocha до запуска версии WASM у меня ушло менее трех дней свободного времени. Поэтому лично я считаю, что двигатель Мокко того года лучше продумывал портативность и ремонтопригодность, имел хорошие инженерные качества. Но базовые конструкции, такие как подсчет ссылок, создавали неотъемлемые узкие места в производительности, которые требовали последующего переписывания, а это уже другая история.

На момент написания этой статьи исполнилось 25 лет со дня официального выпуска JavaScript (совместное объявление Netscape и Sun от 4 декабря 1995 года). Пресс-релиз, посвященный этому событию, также является приложением к JavaScript 20 Years. Как фронтенд-разработчик в Китае, я очень рад видеть, что эта книга получила хороший отклик в Китае (в общей сложности около 60 000 прочтений статей на индивидуальную тематику,Проект перевода GitHub2,2 тысячи звезд). Интересно, что у Брендана Эйча, отца JS, на аватарке в Твиттере тоже написано по-китайски, но, к сожалению, на ней видно только слово «уи», что выглядит так, будто он практикует хуньюань синъи тайцзицюань:

Но благодаря @Gu Yiling я нашел исходное изображение аватара Эйха. Видите ли, китайские иероглифы здесь не метафизика, а кусок куриного бульона для души программиста.Написано же, что «чем больше людей приложат свои усилия, тем полезнее и безвреднее для развития всей экосистемы, открытый исходный код стал своего рода культурой"——

Нашу небольшую практику сегодня можно рассматривать как проявление этой культуры.

Деннис Ритчи, отец языка Си, сказал, что путь к успеху — это удача: «Вы должны оказаться в нужном месте в нужное время, а затем позволить будущим поколениям передать вас по наследству». . Этот язык уже используется в графическом интерфейсе первого пилотируемого космического корабля на космическом корабле SpaceX Dragon и даже собирается летать далеко с космическим телескопом Джеймса Уэбба. Но когда мы оглядываемся на то, с чего все началось, ошибочный движок Mocha 1995 года определенно оказался в нужном месте в нужное время — иначе мы, вероятно, сегодня писали бы VBScript.

Оглядываясь назад на 1995 год, когда 2020 год подходит к концу, можно сказать, что это была невероятная эпоха: была создана ВТО, вступило в силу Шенгенское соглашение, вступило в силу китайское трудовое законодательство, были выпущены Windows 95, Java и JavaScript. И спустя четверть века что-то улучшилось, что-то перевернулось с ног на голову, а что-то уже никогда не вернется.

Забудьте эти плохие вещи. Только сегодня давайте поприветствуем 1995 год, 2020 год и JavaScript.

Портал:Mocha 1995