返回资讯中心
外部精选
软件工程
#开发者实时流

Classetta – Run Classic Apps Directly on Windows/macOS/Linux with Python

A runtime for classic Mac OS (System 1–7) applications with a PyQt6 front end.

Hacker News 最新felixding2026/10/21 · 25 天前30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

A runtime for classic Mac OS (System 1–7) applications with a PyQt6 front end.

Classetta runs an application's own 68k code on an emulated CPU, catches its A-line traps, and answers them with a Toolbox written in Python, with Qt widgets standing in for what QuickDraw used to draw. There is no disassembly or translation step. The program runs as it did on a Mac, and the Toolbox it calls into is ours. Each application's windows sit on your desktop next to your other programs.

Status: real 68k applications run: TeachText, Microsoft Excel, MacWrite, MacPaint and Word 1.0 from 1984, and games such as Creepy Castle and Glider+ among them (see what's been tried).

To run one, see docs/running-apps.md, which covers getting a Mac application to macrt with its resource fork, running it, and reading what it says when it stops.

python -m pip install -e . python -m macrt App.bin # MacBinary, BinHex, AppleDouble… python -m macrt "System 6.0.8.dsk" --app TeachText python -m macrt One.bin Two.hqx # several, side by side

With no file, macrt runs two guest applications written in Python against the same Toolbox API, which show off the window chrome:

pip install -e '.[dev]' python -m macrt # host windows, one global menu bar python -m macrt --menubar window # each window carries its app's menu bar python -m macrt --host screen # everything inside one 1-bit Mac screen python -m macrt --host screen --size 512x342 --classic-menus

The demo launches two apps. Sampler opens every window kind from its Windows menu. Ticker is a clock, so there's a second app to switch to. Ctrl stands in for ⌘ (Command), Alt for Option.

python -m macrt App.bin # MacBinary python -m macrt App.hqx --list # BinHex; list the resources and exit python -m macrt Disk.dsk # the only application on a disk image python -m macrt Disk.dsk --app "Disk:Games:App" # or pick one by name or Mac path python -m macrt App.bin --view # its menus and windows, without its code python -m macrt App.bin --trace-traps # log every trap it calls python -m macrt App.bin --system System.bin # with a real System file unar App.sit.hqx # an archive: unpack it first, then python -m macrt "App Folder/App" # run what came out (App or App.rsrc)

docs/running-apps.md has the whole story: the forms a Mac file can come in, archives and disk images, every option, what works, and the system error IDs.

An application with no 68k code, such as a PowerPC-only one, or any application with --view, gets viewer.py's ResourceViewer in place of its code. ResourceViewer does what the application's startup would do, through the same Toolbox calls: GetNewMBar or GetMenu for the menu bar, then GetNewWindow for the first WIND. A Resources menu opens the other windows, dialogs and alerts.

DisplayConfig (in macrt/ui/display.py) has two independent settings:

Global menu bar. The bar shows the front app's menus. Clicking another app's window brings that app forward (MultiFinder style). It gets suspend and resume events, the bar switches to its menus, and the click itself is not passed on as a mouseDown. With two or more apps running, the System 7 Application menu appears at the right end (Hide App, Hide Others, Show All, then the app list). application_menu="always"|"never" overrides that.

Per-window menu bar. Every document window draws its app's bar under the title bar. When a dialog is in front, the menus stay reachable through the app's other windows. An app with no document windows open gets a small bar-only window titled with its name, so its menus never disappear.

Other options: screen_size; sticky_menus (default on: a quick click on a title leaves the menu open, Mac OS 8 style; off means strictly press-drag-release); and theme.RAINBOW_APPLE for the System 7 color apple.

guest thread (one per app) GUI thread ────────────────────────── ───────────────────────────────── 68k code on the CPU (m68k/cpu.py) └─ A-line trap ─▶ dispatcher (m68k/dispatch.py) └─ glue (m68k/glue_*.py) └─ Toolbox (toolbox/api.py) ─────────▶ GuiBridge.call ──▶ Display (ui/display.py) WaitNextEvent ◀── EventQueue ◀──────────────────────── InputRouter (Qt events) FindWindow, MenuSelect, │ TrackGoAway, DragWindow… ◀── Future ◀── Trackers ◀──────────┘

One processor, shared. The Machine (m68k/runtime.py) is the CPU and the trap stubs; every 68k application has a Runner on its own thread. As under MultiFinder, one application runs at a time. It holds the CPU while its code runs and gives it up in WaitNextEvent, GetNextEvent and the other calls that wait. Whoever takes it next gets its registers and its low-memory globals put back.

Traps. cpu.run() returns at every A-line trap. The dispatcher looks up the trap's signature in m68k/trapdefs.py, which is generated from the Multiversal interface files: where its arguments are (Pascal stack or registers), its result, and its selector for dispatched traps such as _Pack3 or _OSDispatch. Then it calls the Python routine registered under the trap's name with @trap. A known trap with nothing behind it is logged and skipped; one that isn't known at all stops the application with system error 12. SetTrapAddress patches work, and so does calling a trap through the address GetTrapAddress gave. A patch on an OS trap is entered as the Mac's dispatcher enters it, with the registers it saved on the stack, since some patches unwind that frame themselves. GetTrapAddress calls _OSDispatch unimplemented, although it works: programs that look for MultiFinder that way, Microsoft's among them, then make MultiFinder's private calls. Programs that want the Process Manager or temporary memory ask Gestalt.

Code that writes code. The processor translates blocks of 68k code and runs the translation. It notices a store into a block it has translated, but not into the block it's running: a page is mapped to be run from or written to, not both, and a store into the page it's running from starts the processor again at the store (see m68k/cpu.py). Microsoft Word sets a trap word a few instructions ahead and then runs it.

Calling back. Filter procedures, CDEFs, user items, action procedures and patched traps are 68k code that the Toolbox calls. Runner.call() runs a nested CPU loop until the routine returns.

Guest memory is the source of truth. A window is a 156-byte WindowRecord in the app's heap: title, port rectangles, regions, hilite state, zoom states. A MacWindowView only draws whatever the record says. Poke the record the way 68k code would and the widget follows on the next sync.

The chrome is a puppet. Qt never acts on a click by itself. Every click becomes a mouseDown event in the owning app's queue. The app calls FindWindow and then, say, TrackGoAway, and only then does the close box draw pressed. If the app never calls DragWindow, the window doesn't move, just as on a real Mac.

Blocking calls are trackers. MenuSelect, TrackGoAway, TrackBox, DragWindow and GrowWindow start a tracker on the GUI thread and wait on a Future that the tracker settles from mouse events. If the button is already up by the time the app gets there, the tracker sees that, as StillDown would.

ui/chrome.py is the WDEF. It is a set of pure functions for geometry, wHit codes and painting, covering document windows (with or without grow/zoom), dBoxProc, plainDBox, altDBoxProc, movableDBoxProc and rDocProc with its corner variations. Everything is drawn with whole-pixel fillRect, so the output is 1-bit.

macrt/ memory.py guest RAM (big-endian, 24-bit addresses) and record views toolbox/ api.py (the Toolbox calls), events.py, constants.py, types.py system.py (memory map, partitions), memmgr.py (heaps), lowmem.py (low-memory globals), windows.py, menus.py, quickdraw.py (GrafPort, QuickDraw globals, regions), qdraw.py (drawing), screen.py (the screen buffer), pict.py, icons.py, masks.py, color.py, controls.py, dialogs.py, lists.py (ListRec and its cells), resources.py (the Resource Manager), textedit.py, scrap.py, files.py (the File Manager), aliases.py (alias records), sane.py (SANE's arithmetic), intl.py (international formats), script.py (the Script Manager's character tables and breaks), templates.py (WIND, DLOG, ALRT, DITL, CNTL, MBAR, STR#) ui/ display.py, window.py, menubar.py, tracking.py, chrome.py, theme.py, bridge.py, keymap.py, stdfile.py (Open and Save), audio.py (the host's sound output), fonts/ m68k/ cpu.py (unicorn), runtime.py (Machine, Runner, system errors), hardware.py (the VIA and the IWM), sound.py (the Sound Driver and the speaker), sound_manager.py (the Sound Manager's channels), segload.py (CODE 0, the jump table, LoadSeg), dispatch.py and trapdefs.py (trap signatures), glue_*.py (the trap routines: toolbox, os, controls, dialogs, lists, textedit, scrap, files, sony, aliases, stdfile, pict, icons, color, sound, process, aevents, ppc, sane, intl, script) resfork.py the resource fork format, read (every offset checked) and written macfile.py Mac files from the host: MacBinary, BinHex, AppleSingle/Double, emulator side files, HFS images through machfs, MFS images moof.py Applesauce MOOF floppy images: each track's GCR bits decoded back to sectors process.py one running guest app viewer.py the resource listing, and ResourceViewer, which shows an app's menus and windows from its resources demo.py Sampler, Ticker, and the command line

Guest memory

System (in toolbox/system.py) owns 8 MB of emulated RAM:

$000000 low-memory globals: MemTop, ScrnBase, WindowList, MenuList, CurrentA5… $003000 system heap: the Window Manager port, GrayRgn, the System file's resource map and the defproc stand-ins $100000 one partition per app, sized by its SIZE resource (else 1 MB) top the 1-bit screen buffer

A 512×342 screen is where a Mac 512K has it instead, at $7A700 with the alternate buffer at $72700 below it. The system heap ends below both, and the partitions take the rest of RAM above them. Games from 1984 write there without asking ScrnBase.

Each partition is laid out like a classic Mac application space: heap at the bottom, stack below the A5 world, A5 world at the top. The QuickDraw globals (thePort, the patterns, the arrow cursor, screenBits) sit just below A5, and 0(A5) points to thePort, which is where InitGraf leaves it.

toolbox/resources.py is the Resource Manager. As on a Mac, resource maps live in the heap in their on-disk format, so 68k code that walks one finds what it expects, and resources are read in through _ResourceStub, so an application's patch on it, given a map's handle in A4 and an entry in A2, can read them in its own way.

toolbox/templates.py parses WIND, DLOG, ALRT (with its four stages), DITL, CNTL, MBAR and STR#, and applies System 7 positioning words such as centerMainScreen and alertPositionMainScreen. On top of these: GetNewWindow, GetMenu (the MENU resource is the MenuInfo; its MDEF ID becomes the MDEF's handle), GetNewMBar, SetMenuBar, GetMenuBar, GetString and GetIndString. The Dialog and Control Managers build their dialogs, alerts and controls from DLOG, ALRT, DITL and CNTL.

from macrt.macfile import load_file

app = load_file("Hello.bin") # or ._Hello, Hello.rsrc, … display.launch(app.name, guest, app=app)

guest(tb): tb.SetMenuBar(tb.GetNewMBar(128)); w = tb.GetNewWindow(128) …

Fonts

The chrome looks right with Chicago (system font) and Geneva (application font). These aren't bundled. Put .ttf/.otf files in macrt/ui/fonts/ and they load at startup. ChicagoFLF is a freely licensed Chicago clone. With a Mac font present, text is drawn without antialiasing for the crisp 1-bit look. Without one, macrt falls back to Helvetica at 13px with antialiasing on, because outline fonts look broken without it. theme.TEXT_ANTIALIAS = True/False forces either.

Those host fonts stand in for the standard families (Chicago, Geneva, New York, Monaco and the rest). A family an application brings with it, as a FONT, or as an NFNT its FOND lists, is drawn from the font's own bitmap. That matters because such fonts often hold

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →