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
mpw
tool 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
verbatim
folder in their repository into it (so you have e.g. a~/mpw/Environment.text
file, not~/mpw/verbatim/Environment.text
). - Open the file
Environment.text
with a text editor and change theMPWVersion ?= 3.2
line to match whatever version yourMPW Shell
application shows if you chooseGet Info
on it in Finder. - Open the
Interfaces&Libraries
folder from your copy of MPW, and copy theInterfaces
,Libraries
,DebuggingLibraries
andRuntimeLibraries
folders inside it to your~/mpw
folder (so you have e.g. a~/mpw/Interfaces/CIncludes
folder). - open the
MPW
folder of your MPW distribution and copy theTools
folder from there to your~/mpw
folder (so you have e.g. a~/mpw/Tools/AboutBox
file).
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-sources
so it knows this compiler deals with.c
filesmatch-language
tells CLion’s indexer (a version ofclangd
) what to parse these sources as.code-insight-target-name
– I was the least sure about this one.m68k
is the usual abbreviation for the Motorola 68000 series of CPUs of early Macs, but I couldn’t find a list of target platformsclangd
actually supports.include-dirs
tells the indexer where to find the system headers. This means that you can Command-click onWindowPtr
in a source file and it will jump to its definition inMacWindows.h
.defines-text
tells 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 theclang
compiler, so it doesn’t define the right ones for our customSC
compiler, so we have to manually define those that would usually be defined here.
Apple’sConditionalMacros.h
header 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__
to0x0700
instead. 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 thatclang
otherwise displays an error on. Ideally we’d define this to an unusual calling convention soclangd
could 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_BOOL
to1
to keepConditionalMacros.h
from re-definingtrue
andfalse
, but it just hard-codes that value for each compiler, so I couldn’t find a way to override it just for CLion’sclangd
indexer.
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.