Скриншот заголовка
Скриншот заголовка

О проекте

Недавно я опубликовал версию 3.0 Status Line, проекта, который позволяет играть в Zork на Pico-8, на трёх основных операционных системах. После успешного развёртывания (есть ли эмодзи «тьфу‑тьфу‑тьфу»?) я занялся портированием исходного кода оригинальной UNIX z‑machine от Infocom с помощью Cosmopolitan. Примерно за шесть часов свободного воскресенья я портировал его на шесть основных ОС, включая Windows.

В отличие от Status Line, который полагается на хост виртуальной машины Pico-8, этот порт работает нативно на всех поддерживаемых системах. А ещё лучше то, что благодаря магии Cosmopolitan, необходимо поддерживать только один дистрибутив, который может адаптироваться для работы на любой операционной системе, на которой он запущен.

Обновление

Я сделал отдельные исполняемые файлы трилогии Zork, портированные из оригинального исходного кода Infocom UNIX в Cosmopolitan, доступными для Windows/Mac/Linux/bsd для машин arm/x86. Они не требуют ни установки, ни дополнительных файлов для игры.

Вот так  можно загрузить и играть в Zork через CLI:

wget https://github.com/ChristopherDrum/pez/releases/download/v1.0.0/zork1
chmod +x zork1
./zork1

# This one executable runs on any and all targetted platforms
# `zork2` and `zork3` are available, for trilogy completionists
# Windows users, add `.exe` to the downloaded file to make Windows happy
# Linux users with .NET Mono libraries might need to run `sh ./zork1` 

Хотите запустить произвольный файл .z3 text adventure? Загрузите Z-машину отсюда

Вот история о том, как и почему я решил заняться этим проектом, и чему я научился в процессе.

Что такое Z-машина?

Я провёл много времени, разглядывая и размышляя о z-машине Infocom. Если говорить кратко, текстовые приключения Infocom были выпущены как платформенно-независимые игровые файлы, которые запускались в виртуальных машинах, специфичных для каждой поддерживаемой компанией системы. Спецификация этой виртуальной машины известна как «z-машина».

Я не знаю, были ли они первыми, кто выпустил коммерческий продукт с использованием ВМ на домашних компьютерах, но они определённо были одними из первых. В 1980-х годах уникальные компьютерные платформы выпускались с головокружительной скоростью ( Zork 1 был выпущен как минимум на 18 платформах), поэтому было важно иметь возможность быстро переходить на новые системы. Используя ВМ, Infocom могла быстро перенести всю свою библиотеку игр на любую новую машину.

Дополнение: до Zork существовало много виртуальных машин для домашних компьютеров, особенно в среде разработчиков — например, Forth, Apple Pascal и другие. Приключенческие игры Скотта Адамса тоже использовали подобие виртуальной машины, и они вышли раньше Zork.

Сегодня у игроков есть множество современных интерпретаторов Z-машины, но тогда это был проприетарный код. Только Infocom могла создавать интерпретаторы Z-машины, которые они называли ZIP — «Zork Interpreter Program».

ZIP-файлы в основном писались вручную на ассемблере, уникальном для каждой платформы, чтобы выжать максимальную производительность из минимального (16K?! 1.774Mhz?!) оборудования. Но не все они были написаны на ассемблере; также существовал UNIX ZIP, написанный на C. Я совсем не знаю ассемблер, но я достаточно хорошо знаю C. Я думал, соберётся ли этот код C в неизменном виде, как есть? После первой компиляции, ответ был: нет.

Я настойчив, и z-machine — это область, в которой мои знания выше среднего. Возвращение её к жизни показалось мне идеальным проектом, который поможет мне продолжить изучение исторической стороны Infocom, и в то же время он достаточно прост, чтобы позволить мне оценить возможности Cosmopolitan.

Что такое «Cosmopolitan»?

Проще говоря, Cosmpolitan — это детище Джастин Танни, превращающее обычный C в язык «написал один раз — запускай где угодно». Вспомните типичные подходы к такой задаче: Java, WASM и даже сама Z-машина Infocom.

Обычно код пишется на уникальном (иногда предметно-ориентированном) языке и компилируется в специальный байт-код. В случае Java/Z-машины обещание «запуска везде» обеспечивается виртуальной машиной, написанной отдельно для каждой платформы, которая исполняет этот байт-код. Для WASM такой виртуальной машиной обычно выступает браузер, хотя есть и автономные решения.

У Infocom компактный интерпретатор поставлялся на диске вместе с каждой игрой. Запуск был прозрачным: интерпретатор автоматически стартовал приложенную игру. Для пользователя это выглядело как запуск игры, но на самом деле он запускал виртуальную машину, которая запускала игру.

То, что нас объединяет

Cosmopolitan использует другой подход к «написать один раз, запустить где угодно». Вместо того, чтобы создавать виртуальную машину, настроенную на уникальные отличия каждой машины, он переворачивает сценарий и оценивает сходства современных машин; что осталось неизменным с течением времени? Общий интерфейс ABI, построенный на вызовах стандартной библиотеки C, основан на этих общих принципах.

Джастин также заметила, что исполняемые файлы на каждой из платформ имеют больше общего, чем различий. Разработанный ею формат APE (Actually Portable Executable) устроен как .zip-архив (не путать с ZIP Infocom!) и содержит нативный код для всех целевых платформ. После сборки приложение «работает везде», потому что оно нативно везде — виртуальная машина не нужна.

Невероятный APE

APE-файл, собранный с библиотеками Cosmopolitan, можно передать почти любому владельцу 64-битной машины — с любой ОС, от любого производителя — и он запустится. Не нужны отдельные сборки для macOS x64, macOS M-серии, Windows 8/9/10/11, Ubuntu, любого Linux, BSD и т.д. Одна сборка работает почти на любом современном компьютере.

Для этого проекта это означало, что усилия, потраченные на восстановление ZIP Infocom, потенциально могли охватить огромную аудиторию. Кроме того, не пришлось бы возиться с настройками под каждую платформу или сложными makefile-заклинаниями. Я мог сосредоточиться на корректности игры, игнорируя платформенные особенности. Я почувствовал, как это подход разгружает голову.

Дополнительный плюс APE (благодаря его .zip-природе) — возможность создавать самодостаточные исполняемые файлы, которые встраивают z-machine и файл данных игры в один автономный пакет. По-моему, это очень интересный вариант дистрибуции.

Кодирование, как в 1985 году

Моя основная работа связана с Swift и Objective-C, а в проектах для души я обычно использую Lua для Pico-8. Время от времени я погружаюсь в C, но мой опыт ограничен современными стандартами кодинга. До этого я никогда не сталкивался с C в стиле K&R, но код 1985 года быстро заставил меня с ним познакомиться.

Как новичок в стиле K&R, я сразу заметил, насколько многое здесь «предполагается». Например, если у функции не указан возвращаемый тип, по умолчанию предполагается int — даже если функция вообще ничего не возвращает. Некоторые функции действительно возвращают int, другие — char, но не объявляют возвращаемое значение, поэтому вызывающий код считает, что это int (неявное приведение типов).

Параметры функций контролируются только «доверием» к forward‑декларациям — их даже не обязательно объявлять. А зачем вообще использовать общие forward‑декларации, если можно локально объявить внешнюю функцию прямо внутри вызывающей? Условные операторы if с THEN вместо фигурных скобок? Видимо, надо было жить в то время, чтобы понять ту логику.

Дополнение: Комментаторы отмечают, что это не стиль K&R if/then. Скорее, THEN это было определено в исходном заголовке источника как no‑op. Возможно, что это наследие Bergenol.

Всё это к тому, что мне потребовалось время, чтобы научиться читать этот код и понимать его.

Ремонт

Исправления, необходимые для компиляции и работы этого исходного кода, были, честно говоря, довольно простыми. Изменения сводились к трём вещам:

  • Обработка NULL

  • Декларации функций

  • Поддержку устаревших конструкций 

NULL и NULL и снова NULL

В исходной кодовой базе NULL был определён как:

#define NULL 0

И ещё раз позже, в том же файле:

#define NULL 0 --not a typo; it was double-defined.

Конечно, в современных библиотеках C мы определяем NULL как:

#define NULL (void *)0

Так у нас получилось три разных определения NULL в проекте. Весело! Но на самом деле нужно только одно. В оригинальном виде это приводило к ошибкам компиляции в таком коде (хотя if/THEN в стиле K&R работало отлично!):

newlin()
{  
    *chrptr = NULL;        /* indicate end of line */
    if (scripting) THEN
        *p_chrptr = NULL;
    dumpbuf();
}

Во времена, когда писался этот код, предполагалось, что NULL — это просто #define NULL 0. Раз уж разработчики хотели именно этого, так тому и быть:

newlin()
{  
    *chrptr = 0;        /* indicate end of line */
    if (scripting) THEN
        *p_chrptr = 0;
    dumpbuf();
}

Объявления функций (и их отсутствие)

Большинство ошибок компиляции были связаны с вызовом функций, которые ещё не были объявлены. Решить это оказалось несложно. Вот пример из оригинального кода:

ОБНОВЛЕНИЕ:  для этой проблемы полезны флаги-Werror=missing-declarations -Werror=redundant-decls

char *getpag(ptr, page)
char *ptr, *page;
{  
    short blk, byt, oldblk;
    char *makeptr();

    pagfault = 1;                       /* set flag */
    byt = (ptr - dataspace) & BYTEBITS; /* isolate byte offset in block */
    if (curblk) THEN {                  /* in print immediate, so use */
        blk = curblk + 1;               /* curblk to find page */
        curblk++;                       /* and increment it */
        }
    else
        blk = nxtblk(ptr, page);        /* get block offset from last */
    ptr = makeptr(blk, byt);            /* get page and pointer for this pair */
    return(ptr);
}

Хорошо, сначала нам нужно разобраться, как объявляются объявления типов для переданных значений после заголовка функции. Опять же, пропустим это THEN. Вместо этого посмотрите на char*makeptr() — это предварительное объявление функции в локальной области, сама функция определена ниже; её настоящее определение было таким:

char *makeptr(blk, byt)
short blk, byt;
{...}

Обратите внимание: в локальном объявлении нет параметров. Что принимает makeptr()? Судя по всему, мечты и надежды!

Я переключил все заголовки функций на использование современных соглашений, превратив определение makeptr в формат, с которым, я уверен, большинство читающих это хотя бы поверхностно знакомы.

char *makeptr(short blk, short byt)
{...}

Я собрал все заголовки функций в большой блок предварительных объявлений в верхней части файла .c и быстро (нудно) устранил, наверное, 80% предупреждений и ошибок компилятора. С правильными предварительными объявлениями  все локально объявленные переменные вызывали ошибки, что упрощало их поиск и удаление.

Устаревшие конструкции

Времена меняются (а точнее, уже изменились). Некоторые вещи просто перестали работать так, как раньше.

Инициализация srand()

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

mtime()  
{  /* mtime получает машинное время для установки случайного seed. */  
  long time(), tloc;  
    
  rseed = time(tloc); /* получаем системное время */  
  srand(rseed);       /* устанавливаем seed на основе времени */  
  return;  
}  

Я просто заменил это на вариант ниже. «Достаточно хорошо для государственных стандартов», как говорится.

mtime()  
{  
  srand(time(0));  
}  

Клавиша Backspace

На моей клавиатуре Backspace отправляет ASCII 128, но исходный код ожидает только ASCII 8. Пришлось добавить проверку на оба значения, чтобы стирание символов в игровом вводе работало корректно.

Переход с sys/termio.h на termios.h

Старые вызовы управления терминалом были обновлены в соответствии с современными стандартами. Было:

struct termio ttyinfo;  
ttyfd = fileno(stdin);        /* получаем файловый дескриптор */  
if (ioctl(ttyfd, TCGETA, &ttyinfo) == -1) THEN  
  printf("\nIOCTL - TCGETA failed");  

Стало:

struct termios ttyinfo;  
ttyfd = fileno(stdin);        /* получаем файловый дескриптор */  
if (tcgetattr(ttyfd, &ttyinfo) == -1) {  
  printf("\ntcgetattr failed");  
}  

Компиляция с Cosmopolitan

Благодаря cosmocc — инструменту компиляции от Cosmopolitan — Z‑машина заработала на 6 современных ОС одной командой:

cosmocc -o zm phg_zip.c -mtiny  

Никаких makefile, никаких махинаций с компиляцией на уровне системы, никакого условного кода с моей стороны. Почти до безобразия просто, Cosmopolitan позволил использовать аппаратно‑независимый ABI и внести лишь минимальные (часто косметические) правки в исходный код.

Признаюсь, когда знаменитое начало Zork ожило прямо из архивного кода его создателей — это было нечто особенное. Проведя столько времени в Status Line за эти годы, я ожидал, что снова буду пресыщен «West of House». Честно говоря, всё было совсем наоборот. Зная всю подноготную этого кода и его место в истории геймдева, я ощутил это ещё острее — будто впервые.

Но мы можем пойти дальше

APE-файлы обладают одной скрытой суперспособностью. Z-машина Infocom принимает флаг -g в командной строке, за которым следует путь к файлу .z3 для запуска игры. Но на самом деле можно встроить этот флаг и сам игровой файл прямо в APE.

При запуске программа проверит свои внутренние структуры на наличие аргументов командной строки и связанных данных.

Представьте себе macOS-приложение — по сути, это просто папка с исполняемыми файлами и данными, которую можно легко просмотреть. С APE мы можем сделать то же самое.

Для этого нам нужно два компонента:

Интеграция .args и игровых данных

Создайте файл с именем .args, который содержит -g/zip/game_filename.z3. Это аналогично запуску игры через командную строку, но с префиксом zip/ — это внутренний относительный путь, по которому будут храниться данные. Чтобы упростить дальнейшие манипуляции с исполняемым файлом, переименуйте zm в zm.zip. Скопируйте свой .args файл и связанный с ним .z3 файл игры в zm.zip файл с помощью команды:

zip -j zm.zip .args /path/to/game_filename.z3

Исполняемый файл нужно настроить на поиск встроенных аргументов. В Cosmopolitan для этого есть удобная функция, которую нужно вызвать в самом начале main():

#include 
int main(int argc, char **argv)  
{
int _argc = cosmo_args("/zip/.args", &argv);
if (_argc != -1) argc = _argc;
...

Это заполнит argv встроенными аргументами, как если бы их передали вручную.

В репозитории есть Makefile с командой embed, которая автоматизирует всю эту рутину. После сборки переименуйте zm.zip в желаемое название (например, zork.exe для Windows).

Несколько непрошеных советов

1. Выбирайте знакомые проекты

Для первого опыта портирования старого UNIX-кода на современный C и изучения Cosmopolitan важно работать с тем, что вам хорошо знакомо. Я отлично понимал, как должна работать Z-машина, как она выглядит и что от неё ожидать. Это помогло заранее оценить масштаб задачи, быстро замечать ошибки (например, fflush(stdout)), интуитивно находить решения и прогнозировать методы исправления целых категорий ошибок.

2. Не бойтесь ошибок компиляции

Когда компилятор выдаёт километровый список ошибок — не паникуйте. Воспринимайте это как чеклист для исправлений. Можно отключить предупреждения флагом -w, чтобы сосредоточиться только на ошибках. Или исправлять проблемы по одной, постепенно привыкая к коду.

Для продакшена -w не подходит, но для экспериментов — отлично! Пользуйтесь, если вы заинтересованы только в том, чтобы это что‑то запустить и оно заработало ради удовольствия, этот флаг определённо может сократить список «что нужно сделать» до чего‑то управляемого, пока вы изучаете исходный код.

Наконец, я действительно не могу не подчеркнуть простоту разработки, которую предоставил Cosmopolitan. Компилятор Cosmocc, сам по себе построенный на gcc, является APE и как таковой является самодостаточной экосистемой компиляции, связанной с заменой Cosmopolitan Libc на стандартную библиотеку C.

В прошлом я потратил так много времени на настройку $PATH, размещение библиотек в нужных местах, установку зависимостей, попытки заставить MSYS2 работать должным образом и многое другое, что удобство единого приложения APE, унифицированного на всех моих машинах, вызывало у меня чувство: «Да, именно так все и должно быть. Это должно быть так просто». Один файл — и всё просто работает. После многих лет борьбы с настройками окружения это ощущается как глоток свежего воздуха.

Надеюсь, у вас будет такой же позитивный опыт. Cosmopolitan действительно делает кросс‑платформенную разработку настолько простой, насколько она должна быть.

Играем в игры Z-Machine

Предварительно скомпилированная APE-сборка z-machine для 64-битных систем доступна на моем github вместе с заметками о том, как её использовать. Отдельные сборки трилогии Zork также доступны там, чтобы продемонстрировать мощь формата APE. Помните, этот проект по сути отражает состояние кода в 1985 году; я не даю никаких гарантий его надёжности или точности! Впрочем, не ради этого стоит его изучать. Если вы серьёзно хотите играть в интерактивную фантастику, есть множество вариантов получше, чем этот порт.

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

 И в этом, полагаю, есть своя особая значимость.

Спасибо за внимание, ваш Cloud4Y. Читайте нас здесь или в Telegram‑канале!

Комментарии (0)