[Перевод] Ретрокодинг на Macintosh System 7.5, Think C и ResEdit
Большинство современных программистов привыкли использовать инструменты автодополнения кода и новомодные ИИ-штучки а-ля Copilot. Они стали нормой.
Эти инструменты настолько удобны, что мы начали забывать о боли и борьбе, которые сопровождали процесс разработки в далеком прошлом. Разве что вы, как и я, не увлекаетесь компьютерной историей и ретро-программированием в частности.
Классические компьютеры Macintosh становятся все популярнее среди коллекционеров. В моей личной коллекции имеются и iMac G3, и горячо любимый мной Macintosh SE 30, и Apple Newton. Я собираю их не ради развлечения — я чувствую, как стремительно мир утрачивает знания об истории вычислительной техники. Особенно это касается программирования. Уму непостижимо, сколько сил приходится приложить, чтобы найти все необходимые средства разработки и документацию тех лет.
Почему и кому это нужно
Оригинальный Macintosh и одноименная ОС поступили в продажу в январе 1984 года. Долгих 17 лет ОС Macintosh будет жить, пока в 2001 году ей на смену не придет Mac OS X (NeXTSTEP). Вместе с MacOS 9.2, последней версией системы Macintosh, исчезли тысячи прикладных программ и инструментов разработки.
Среда разработки в системе Macintosh
Зачем кому-то сегодня понадобилось ретропрограммирование?
Вот несколько причин:
Сохранить культуру разработки ПО, ее историческое наследие, извлекать уроки из прошлых успехов и неудач.
Понять, почему те или иные практики, инструменты и методологии разработки применяются и сегодня.
Черпать вдохновение из опыта прошлых поколений, сохранять их знания, практики и идеи. И так двигаться дальше.
История и аутентичность
История развития ПО позволяет разработчикам по достоинству оценить вклад тех, кто стоял у его истоков. Осознать себя частью сообщества с богатым опытом решения сложных проблем, двигателем технологического прогресса. Особенно это касается новичков, которым и так нелегко разобраться с огромным количеством стеков, цепочек инструментов, нюансов операционных систем, API и парадигм программирования.
Интро презентации HyperCard на WWDC '91 (ISO есть в архиве)
Образцы автомобильных или, скажем, промышленных технологий по окончании срока поддержки, бывает, попадают в музеи, а компьютерная техника и программное обеспечение часто заканчивают путь на свалке. Мы, сообщество разработчиков программного обеспечения, не должны с этим мириться. Нельзя допускать, чтобы записи WWDC '91 были закопаны в недрах Стенфордской библиотеки с доступом по личному запросу.
Надеюсь, эта статья внесет небольшую лепту в формирование исторического контекста для программистов, особенно тех, кто в настоящее время работает с macOS, iOS, iPadOS, watchOS или tvOS.
Подготовка к запуску Macintosh 7
Прежде чем приступить к работе, понадобится целый ряд инструментов. Во-первых, сама Macintosh System 7. Во-вторых, ResEdit (версия 2.1.3) и Think C (я рекомендую версию 5). Существуют также различные способы запустить System 7 в виртуальном окружении. А для работы с реальной, физической ОС уже понадобится старенький Macintosh.
Возможно, вам пригодятся эти ссылки:
Infinite Mac System 7 — работает в браузере и уже имеет на борту все необходимое.
Basilisk II — запускается на современном Mac.
UTM with QEMU — запускается на современном Mac.
Все необходимые приложения можно найти:
Книги по программированию свободно распространяются на Vintage Apple.
Мой личный фаворит — «The Macintosh C Programming Primer 1992».
Разобраться, как устроен ResEdit можно с «ResEdit Complete 1991».
ResEdit 2.1.3 и Think C 5 в Infinite Mac’s System 7
Когда все инструменты подготовлены, можно приступать к созданию своих приложений для Macintosh. Не помешает завести на рабочем столе папку, в которую потом можно будет сохранять проекты. И Think C, и ResEdit разрешают пользователю устраивать бардак и не следят за соблюдением норм и правил.
Самая простая программа: Hello Macintosh
Проверить, правильно ли функционирует инструментарий можно, если написать простую программку «Hello Macintosh» для командной строки. Поскольку на Macintosh командной строки фактически нет, мы выведем текст в Output Window. Для проектов Think C используется расширение файла .∏. На Mac можно ввести символ ∏, используя сочетание option + shift + p. При отсутствии расширения файл проекта не откроется. Кроме того, файл ResEdit в идеале должен иметь то же имя, что и сам проект. Для ресурсного файла проекта HelloMacintosh.∏ это будет HelloMacintosh.∏.rsrc. Также убедитесь, что все файлы находятся в одной папке.
Добавление файла в проект Think C
Если создать файл «HelloMacintosh.c», он окажется обычным текстовым файлом и не будет ассоциирован с проектом. Его придется добавить в проект отдельно. Если сейчас попытаться запустить проект, то он завершится с ошибкой линкера, так как вы не импортировали библиотеку ANSI C.
Библиотека ANSI C в составе Think C
Как только библиотека ANSI будет подключена к проекту, можно будет запустить его, и на экране появится окно с текстовым выводом «Hello Macintosh». Тем самым мы проверили, что система работает, и можно компилировать базовые приложения на C.
Запуск скромного «Hello Macintosh» с помощью Think C
Это, конечно, не самый вдохновляющий старт. Хочется создать что-то более полноценное, желательно с графическим интерфейсом. Для этого нужно с помощью ResEdit создать файл ресурсов и добавить в него соответствующие элементы.
ResEdit: окна, диалоги и кнопки
ResEdit — это независимая программа, которая не привязана к Think C. Она будет работать и с другими приложениями, например с Macintosh Programmer’s Workshop (сокращенно MPW) от Apple. В ResEdit можно добавлять элементы и строить пользовательский интерфейс в графическом виде.
ResEdit 2.1.3 на Macintosh System 7.5
ID используются в ResEdit в качестве ссылки для доступа кода к ресурсам. Подход, который в то время разделяли все операционные системы, включая Palm OS и Windows. Я не буду вдаваться во все подробности работы с ResEdit и вместо этого посоветую вам прочесть книги, упомянутые ранее. В них обеих подробно описаны ResEdit и работа с ним.
Простое диалоговое приложение
Вот приложение, которое показывает окно (ресурс «WIND») и закрывает его при нажатии пользователем любой кнопки. Как только окно будет закрыто, на экране появится диалог (ресурсы «DLG» и «DITL») с сообщением.
// references the resource id 128
#define kBaseResID 128
#define kMoveToFront (WindowPtr)-1L
#define kHorizontalPixel 30
#define kVerticalPixel 30
// definition of the functions
void ToolBoxInit(void);
void WindowInit(void);
void ShowDialog(void);
// our main function
void main(void){
ToolBoxInit();
WindowInit();
// closes the window when any button is pressed
while(!Button());
// show the DLG and DITL when a button is pressed.
// WARNING: if the DITL doesn't have
// any button, it may freeze Basilisk II
NoteAlert(kBaseResID, nil);
}
// initialises the Graf of the Gestalt ;)
// meaning the graphics of the machine
void ToolBoxInit(void){
InitGraf(&thePort);
InitFonts();
InitWindows();
InitMenus();
TEInit();
InitDialogs(nil);
InitCursor();
}
// actually runs our main window
void WindowInit(void){
WindowPtr window; // window pointer for our window
ControlHandle button; // control handle that is the button
Rect buttonPos; // rectangle defining where the button is
// gets the window from the resources using the id of 128
window = GetNewWindow(kBaseResID, nil, kMoveToFront);
// beeps and crashes if the window resource isn't there
if(window == nil){
SysBeep(10);
ExitToShell();
}
// shows our window
ShowWindow(window);
SetPort(window);
MoveTo(kHorizontalPixel,kVerticalPixel);
DrawString("\pHello Medium reader. Subscribe to my articles!");
// setting the position of our button
buttonPos.top = 60;
buttonPos.left = 23;
buttonPos.bottom = 90;
buttonPos.right = 140;
button = NewControl(window, &buttonPos, "\pSubscribe now!", true, 0, 0, 0, pushButProc, 0);
if(button == nil){
SysBeep(10);
ExitToShell();
}
ShowControl(button);
}
Помимо ресурсного файла, в проект нужно будет импортировать библиотеку MacTraps. В ней содержатся заголовочные файлы и информация для графики (Graf) и рабочего окружения (Gestalt). После компиляции приложение выведет на экран окно с кнопкой и выдаст NoticeAlert после нажатия на нее.
Простое диалоговое приложение на System 7.5.3
Это нехитрое приложение, которое наглядно демонстрирует, как устроена в Macintosh работа с пользовательским интерфейсом. В книгах по программированию, которые можно найти на Vintage Apple, также есть примеры работы с графикой, принтерами и многими другими периферийными устройствами и системными библиотеками.
Сетевые приложения и MacTCP
Вот еще одна очень важная фишка. Во всех этих книгах рассматриваются самые разнообразные темы, но ни одна из них не затрагивает программирование приложений, использующих сеть или интернет. Мои Mac SE 30 и iMac G3 подключены к локальной сети и, соответственно, к интернету. Было достаточно сложно вывести SE 30 в сеть, но еще сложнее написать для него приложение с поддержкой доступа в интернет. Работа в сети на Mac осуществляется с помощью библиотеки под названием MacTCP. В начале 90-х годов эта библиотека стоила сотни долларов.
Статья Стива Фалкенбурга о MacTCP, опубликованная весной 1991 года
Я очень рекомендую статью Стива Фалкенбурга «MacTCP Cookbook: Constructing Network-Aware Applications» в журнале Develop Issue 6.
Покажу полный пример Стива по реализации протокола Finger с помощью MacTCP.
Я позволил себе немного подправить форматирование, чтобы оно соответствовало современной схеме подсветки синтаксиса.
/* MacTCP finger client */
/* written by Steven Falkenburg */
/* */
#include
#include
#include
#include
#include
#include "CvtAddr.h"
#include "MacTCPCommonTypes.h"
#include "TCPHi.h"
#include "TCPPB.h"
/* constants */
#define kFingerPort 79 /* TCP port assigned for finger protocol */
#define kBufSize 16384 /* Size in bytes for TCP stream buffer and receive buffer */
#define kTimeOut 20 /* Timeout for TCP commands (20 sec. pretty much arbitrary) */
/* function prototypes */
void main(int argc, char *argv[]);
OSErr Finger(char *userid, char *hostName, Handle *fingerData);
OSErr GetFingerData(unsigned long stream, Handle *fingerData);
void FixCRLF(char *data);
Boolean GiveTime(short sleepTime);
/* globals */
Boolean gCancel = false; /* This is set to true if the user cancels an operation. */
/* main entry point for finger */
/* */
/* usage: finger @ */
/* */
/* This function parses the args from the command line, */
/* calls Finger() to get info, and prints the returned info. */
void main(int argc, char *argv[]) {
OSErr err;
Handle theFinger;
char userid[256], host[256];
if (argc != 2) {
printf("Wrong number of parameters to finger call\n");
return;
}
sscanf(argv[1], "%[^@]@%s", userid, host);
strcat(userid, "\n\r");
err = Finger(userid, host, &theFinger);
if (err == noErr) {
HLock(theFinger);
FixCRLF(*theFinger);
printf("\n%s\n", *theFinger);
DisposHandle(theFinger);
} else {
printf("An error has occurred: %hd\n", err);
}
}
/* Finger() */
/* This function converts the host string to an IP number, */
/* opens a connection to the remote host on TCP port 79, sends */
/* the id to the remote host, and waits for the information on */
/* the receiving stream. After this information is sent, the */
/* connection is closed down. */
OSErr Finger(char *userid, char *hostName, Handle *fingerData) {
OSErr err;
unsigned long ipAddress;
unsigned long stream;
/* open the network driver */
err = InitNetwork();
if (err != noErr) return err;
/* get remote machine's network number */
err = ConvertStringToAddr(hostName, &ipAddress);
if (err != noErr) return err;
/* open a TCP stream */
err = CreateStream(&stream, kBufSize);
if (err != noErr) return err;
err = OpenConnection(stream, ipAddress, kFingerPort, kTimeOut);
if (err == noErr) {
err = SendData(stream, userid, (unsigned short)strlen(userid), false);
if (err == noErr) err = GetFingerData(stream, fingerData);
CloseConnection(stream);
}
ReleaseStream(stream);
return err;
}
OSErr GetFingerData(unsigned long stream, Handle *fingerData) {
OSErr err;
long bufOffset = 0;
unsigned short dataLength;
Ptr data;
*fingerData = NewHandle(kBufSize);
err = MemError();
if (err != noErr) return err;
HLock(*fingerData);
data = **fingerData;
dataLength = kBufSize;
do {
err = RecvData(stream, data, &dataLength, false);
if (err == noErr) {
bufOffset += dataLength;
dataLength = kBufSize;
HUnlock(*fingerData);
SetHandleSize(*fingerData, bufOffset + kBufSize);
err = MemError();
HLock(*fingerData);
data = **fingerData + bufOffset;
}
} while (err == noErr);
data[0] = '\0';
HUnlock(*fingerData);
if (err == connectionClosing) err = noErr;
}
/* FixCRLF() removes the linefeeds from a text buffer. This is */
/* necessary, since all text on the network is embedded with */
/* carriage return linefeed pairs. */
void FixCRLF(char *data) {
register char *source, *dest;
long length;
length = strlen(data);
if (*data) {
source = dest = data;
while ((source - data) < (length - 1)) {
if (*source == '\r') source++;
*dest++ = *source++;
}
if (*source != '\r' && (source - data) < length) *dest++ = *source++;
length = dest - data;
}
*dest = '\0';
}
/* This routine would normally be a callback for giving time to */
/* background apps. */
Boolean GiveTime(short sleepTime) {
SpinCursor(1);
return true;
}
Статья Стива — хороший способ поиграться с MacTCP, но для глубокого погружения вам понадобится нечто большее. Руководство разработчика MacTCP. На поиски этого мануала у меня ушло порядка 2 месяцев. Его дал мне один неизвестный пользователь с форума mac68k.
Я загрузил его в Internet Archive, чтобы сохранить знания о MacTCP для всех желающих.
Все необходимые библиотеки устанавливаются сетевым драйвером. За подробной информацией можно обратиться к руководству по MacTCP.
Пример полноценного приложения на MacTCP можно найти в клиенте Wikipedia Джошуа Стейна для классической MacOS.
Создаем «релизную» версию приложения под Macintosh
Мы изучили процесс написания программы и запуска тестовой сборки. Старшее поколение сейчас улыбнется, а я опишу процесс создания «релизной» версии приложения. Что такое релиз? Фактически это готовая дискета с программой. Скомпилированный бинарник, который выдает Think C, можно скинуть на дискету.
Программирую в CodeWarrior 8 Gold на своем iMac G3 с macOS 9.2
Вставьте дискету в Macintosh и скопируйте бинарный файл в папку с программами. Вот, собственно, и все. Распространение приложений для Macintosh на заре становления компьютеров происходило посредством почты. Люди отправляли деньги, а взамен получали конверт с дискеткой. В качестве альтернативы можно было пойти в компьютерный магазин, или же скачать приложение через интернет. Но этот способ появился гораздо позднее.
Если у вас есть более современный компьютер, вроде моего iMac G3, то вы можете использовать Rumpus FTP на iMac G3 и FileZilla на MacBook Pro для передачи файлов между вашим актуальным Mac и ретро-машиной.
Пара слов в заключение
Посмотришь на ResEdit, интерфейс ранних Macintosh 1984 года — и весь покрываешься мурашками. Мы постоянно используем кнопки, чекбоксы, текстовые поля и другие элементы пользовательского интерфейса, премьера которых на массовом рынке состоялась почти 40 лет назад. Macintosh вышел на рынок 24 января 1984 года. Это на 444 дня раньше, чем я родился. Это дальше, чем 2060 год от сегодняшнего дня. На системах Macintosh начала 90-х годов выполнение простого метода HTTP GET по сети с помощью веб-сервера представляло собой как минимум несколько сотен строк кода.
Очень многое можно почерпнуть у старого Macintosh, если не полениться и попробовать написать что-то под него. Когда вы разрабатываете приложение для iOS с помощью SwiftUI, помните о том, с чего все начиналось. Вы создаете программу мечты наших предшественников, которые лишь грезили о том, чтобы носить Macintosh у себя в кармане. iPhone, наследник этого компьютера, обладает мощью и силой 215 машин прошлого (iPhone 15 — многоядерный CPU на 2,4 ГГц против одноядерного Mac SE 30 на 16 МГц).
Мы — инженеры-программисты, разработчики, простые кодеры, изобретатели — оказались ответственными за воплощение в жизнь мечтаний наших предшественников.