Cross-compiling Classic Mac apps on MacOS X
I like to do some retro programming, but SheepShaver, the best Mac emulator out there,
has a bug that makes copy and paste not function, so is kind of hard to use. I was
recently made aware
that there is a tool named mpw (lowercase) that emulates just enough of classic MacOS
to run Apple’s MPW compiler suite’s command line tools on MacOS X. So I thought I’d give
it a try and set that up.
Installing MPW on OS X
- Build the
mpwtool according to the instructions in their Readme.md, basically:mkdir build cd build cmake .. make - Get a copy of MPW for classic MacOS (their Acquiring MPW page page lists some places where you can still get it if you haven’t kept your original E.T.O. or CodeWarrior CDs).
- Create a folder
~/mpw. - Copy the files from the
verbatimfolder in their repository into it (so you have e.g. a~/mpw/Environment.textfile, not~/mpw/verbatim/Environment.text). - Open the file
Environment.textwith a text editor and change theMPWVersion ?= 3.2line to match whatever version yourMPW Shellapplication shows if you chooseGet Infoon it in Finder. - Open the
Interfaces&Librariesfolder from your copy of MPW, and copy theInterfaces,Libraries,DebuggingLibrariesandRuntimeLibrariesfolders inside it to your~/mpwfolder (so you have e.g. a~/mpw/Interfaces/CIncludesfolder). - open the
MPWfolder of your MPW distribution and copy theToolsfolder from there to your~/mpwfolder (so you have e.g. a~/mpw/Tools/AboutBoxfile).
Your MPW should now be ready.
Building the standard SillyBalls example that comes with MPW
Create a folder named mpw-sillyballs to hold your project (you can choose any name here,
but that’s what I’ll call it going forward).
Obtain the C source code
Find the SillyBalls.c example. For MPW 3.5, it is at MPW/Examples/CExamples/SillyBalls.c.
Copy it into a the mpw-sillyballs folder.
Creating a resource file
Every application on classic MacOS needs a few resources that describe the application and
its UI. Those are usually defined in a resource file. You can create this resource file in
a text description language named Rez. We’ll create a minimal resource file named
SillyBalls.r that just tells the operating system about our application’s abilities:
#include <SysTypes.r>
#include <Types.r>
/* here is the quintessential MultiFinder friendliness device, the SIZE resource */
resource 'SIZE' (-1) {
dontSaveScreen,
acceptSuspendResumeEvents,
enableOptionSwitch,
canBackground, /* we can background; we don't currently, but our sleep value */
/* guarantees we don't hog the Mac while we are in the background */
multiFinderAware, /* this says we do our own activate/deactivate; don't fake us out */
backgroundAndForeground, /* this is definitely not a background-only application! */
dontGetFrontClicks, /* change this if you want "do first click" behavior like the Finder */
ignoreChildDiedEvents, /* essentially, I'm not a debugger (sub-launching) */
is32BitCompatible, /* this app is safe to run in 32-bit address space */
reserved,
reserved,
reserved,
reserved,
reserved,
reserved,
reserved,
23 * 1024, /* 23kb Preferred max. RAM */
35 * 1024 /* 35kb Minimal RAM limit */
};
Create the Makefile for building this app
# This should point to wherever you've built the 'mpw' tool from this repository:
MPW=~/Programming/mpw/build/bin/mpw
RINCLUDES=~/mpw/Interfaces/RIncludes
# 'SILB' is the unique "creator code" for this Silly Balls app, and used to associate icons
# with the app and its documents, and tell Finder to use this app to open a file. Make up
# your own unique 4-character code for your app here. All-lowercase codes are usually used
# by Apple, so use at least one uppercase letter.
LDFLAGS =-w -c 'SILB' -t APPL \
-sn STDIO=Main -sn INTENV=Main -sn %A5Init=Main
PPC_LDFLAGS =-m main -w -c 'SILB' -t APPL
LIBRARIES={Libraries}Stubs.o \
{Libraries}MacRuntime.o \
{Libraries}IntEnv.o \
{Libraries}Interface.o \
{Libraries}ToolLibs.o \
{CLibraries}StdCLib.o
PPC_LIBRARIES={SharedLibraries}InterfaceLib \
{SharedLibraries}StdCLib \
{PPCLibraries}StdCRuntime.o \
{PPCLibraries}PPCCRuntime.o
TOOLBOXFLAGS=-d OLDROUTINENAMES=1 -typecheck relaxed
SOURCES=SillyBalls.c
OBJECTS=$(SOURCES:%.c=obj/%.68k.o)
PPC_OBJECTS=$(SOURCES:%.c=obj/%.ppc.o)
RFILES=SillyBalls.r
EXECUTABLE=SillyBalls
all: prepass bin/$(EXECUTABLE).ppc bin/$(EXECUTABLE).68k
prepass:
mkdir -p obj bin
bin/$(EXECUTABLE).ppc: $(PPC_OBJECTS)
$(MPW) PPCLink $(PPC_LDFLAGS) $(PPC_OBJECTS) $(PPC_LIBRARIES) -o $@; \
Rez -rd $(RFILES) -o $@ -i $(RINCLUDES) -append
bin/$(EXECUTABLE).68k: $(OBJECTS)
$(MPW) link $(LDFLAGS) $(OBJECTS) $(LIBRARIES) -o $@
Rez -rd $(RFILES) -o $@ -i $(RINCLUDES) -append
obj/%.68k.o : %.c
$(MPW) SC $(TOOLBOXFLAGS) $< -o $@
obj/%.ppc.o : %.c
$(MPW) MrC $(TOOLBOXFLAGS) $< -o $@; \
clean:
rm -rf bin obj
Building the app
Just type make all in your mpw-sillyballs directory. The Makefile will now create a
bin/SillyBalls.68k and a bin/SillyBalls.ppc executable.
Integrating with CLion
Since we’re running on a modern OS, I thought I’d try if I could integrate the compiler with a modern IDE. I own CLion already and love its code navigation and refactoring features, so I thought I’d use CLion’s custom compiler support.
So as the CLion docs say, I created a compiler definition YAML file and used the little gear popup’s “Preferences…” menu item in the upper right of the CLion window to tell my project about this compiler definition file.
It gave the error “No compilation commands found.” One Stackoverflow post later, it was
revealed that CLion’s Makefile parser is probably not smart enough to recognize
$(MPW) SC as the same as mpw SC and the like. So I went and created a little shell
script sc.sh to wrap the emulated compiler call as a single command:
#!/bin/zsh
# This script is needed so the CLion IDE will recognize the compiler and run it.
~/Programming/mpw/build/bin/mpw SC $@
And then updated the Makefile to call that instead of $(MPW) SC, and updated the YAML
file to its final form:
compilers:
- description: "MPW SC"
match-sources: ".*\\.c"
match-language: "C"
match-compiler-exe: "(.*/)?sc.sh"
code-insight-target-name: m68k
include-dirs:
- ${user-home}/mpw/Interfaces/CIncludes
defines-text: "
#define __SC__ 0x0801
#define MPW_C 1
#define OLDROUTINENAMES 1
#define pascal
"
The important parts (besides using the shell script) here are:
match-sourcesso it knows this compiler deals with.cfilesmatch-languagetells CLion’s indexer (a version ofclangd) what to parse these sources as.code-insight-target-name– I was the least sure about this one.m68kis the usual abbreviation for the Motorola 68000 series of CPUs of early Macs, but I couldn’t find a list of target platformsclangdactually supports.include-dirstells the indexer where to find the system headers. This means that you can Command-click onWindowPtrin a source file and it will jump to its definition inMacWindows.h.defines-texttells CLion to pretend some constants had been defined by the compiler. Most compilers define some constants that let you detect which compiler it is. But clangd is based on theclangcompiler, so it doesn’t define the right ones for our customSCcompiler, so we have to manually define those that would usually be defined here.
Apple’sConditionalMacros.hheader looks at these constants to set up some more standardized constants no matter what compiler you’re using, so this is a good place to find out what constants you need and what values they should have.
For example, if you were setting up the PowerPC compilerMrC, you’d probably have to define__MRC__to0x0700instead. I just picked the highest value that header had for that constant. I should probably actually run this compiler in the emulator and see what the actual value of my version is. I also decided to define awaypascal, because it is a keyword thatclangotherwise displays an error on. Ideally we’d define this to an unusual calling convention soclangdcould possibly error if we pass a Pascal function through a mis-matched function pointer with C calling conventions, but I didn’t yet get around to doing all that. Also, I tried to defineTYPE_BOOLto1to keepConditionalMacros.hfrom re-definingtrueandfalse, but it just hard-codes that value for each compiler, so I couldn’t find a way to override it just for CLion’sclangdindexer.
In any event, this made CLion happy, and not only was I now able to jump to system headers
using CLion’s standard navigation, it also now let me use the little “Hammer” icon to
build my app with make all, and the Build > Clean menu item to run make clean.
Now to set up things for the C++ compiler too, and then I can get coding.
PS - Some of the above description may look eerily like the `mpw` project's Wiki. That's because there was little documentation on how to initially set up the tool, and so I contributed an early draft of this article to their Wiki.