David Cook
Well-known member
Note: This is only going to be of interest to programmers trying to write code that works on the Mac’s earliest operating systems. I am posting this information so that it doesn’t get lost to time.
Apple Macintosh System 1 (aka 0.97) released on January 24, 1984. Metrowerks CodeWarrior Gold 11 released on December 31, 1996, which is about 13 years later. By then, the last 68K Macintosh, the PowerBook 190, had been discontinued. For some reason, I thought it would be a good idea to use CodeWarrior 11 to write an application that could run on a Mac 128K with System 1.
Tiny Transfer is my little application for sending files back and forth over a serial cable. Its purpose is to make it easier when repairing, testing, and playing with my old computers. Thus, Tiny Transfer needs to work on all Macs and all Mac OSes.
There are a number of challenges to writing an application for Macintosh System 1.0:
First Crash: Compiler startup code
Metrowerks apparently did not expect System 1 would be targeted by their development environment. I guess System 1 was no longer a commercially large platform in 1996? Compilers insert startup code that runs before your code when your application launches. This startup code initializes global variables and even patches a few traps. Using MacsBug, I determined that a crash was happening before my code even started.
Fortunately, Metrowerks provides the source for the startup code, and they even provide the project file to let you build a custom version. Looking through the source code, we can spot a couple of interesting items.
Rather than a major rewrite, I simply check for a 64K ROM after initializing the globals and skip over all the other stuff. This allows it to work on the earliest machines without sacrificing this functionality in later systems. Set the linker to a single segment so the patches to the Segment traps aren’t needed. Add the custom startup lib and set the startup configuration to custom.
I've attached my customized Appl68KStartup.c source file in case anyone else needs it.
Second Crash: MemError() isn’t set
Like any good defensive Macintosh programmer, I call MemError() to check for errors after making a memory manager call. I assumed, wrongly, that the returned ptr or handle is garbage if MemError() reported an error. In that case, I set the pointer to nil. That exercised a piece of code that always assumes there is enough memory for a little allocation at initialization time. There was enough memory, but MemError was a lie. My dereference of a pointer that I set to nil resulted in a crash.
MemError is not actually set by the Memory Manager in the 64K ROMs. Instead, Pascal compiler ‘glue code’ grabs the D0 return value and stuffs it into the MemError global variable, such that the following application call to MemError() works. The CodeWarrior developers weren’t unaware of that (or didn’t bother), as Apple fixed this in the 128K ROMs. See IV-79.
As a developer, you can just just check whether a handle or ptr is not nil when returned. If it is nil, set MemError yourself.
Third Crash: Traps are less tolerant
Usually, I check whether a pointer is nil or a port is open before making a system call with it. I missed one spot where my code asks the serial port if there are incoming bytes. This worked in later systems, because the OS checked the port parameter and noticed it wasn’t open. In System 1, the call just crashes.
The correct fix is to make sure you don’t call the System with a closed port value.
Fourth Crash: WaitNextEvent?
Okay, this actually happened in System 3.3 when I thought using an emulated HD20 would provide more space to exercise my application. The Hard Disk 20 init startup provides a 400K/800K floppy and HD20 driver, along with the hierarchical file system (HFS).
My application started up, showed the menu bar, and then crashed after about 4 seconds with a bomb box. It was system error 28, which says that the heap and stack overlapped. I assumed a loop was filling memory or recursion was happening on a callback.
While stepping through the code with MacsBug, it showed my application calling WaitNextEvent, not GetNextEvent. What? That’s only available with Multifinder or System 7.
Surprise! The OS trap for HFSDispatch (implemented by the Hard Disk 20 init) has the same final digits as WaitNextEvent. Checking the smushed-together 64K ROM trap table wrongly indicates WaitNextEvent is implemented, when it’s actually the HFSDispatch patch. Calling the wrong routine with unexpected parameters every loop eventually fills up memory via the stack.
Macintosh Technical Note #158 provides the solution: WaitNextEvent is never supported on 64K ROMs.
Fifth Crash: HOpenResFile and HCreateResFile
Don’t get fooled by the letter ‘H’ -- these are not HFS traps. That is, they are not part of the HD20 Init or MacPlus ROM that brought HFS to the Macintosh. You need to check for the existence of these traps specifically, not the existence of HFS. CodeWarrior has nice glue code that does this work for you, and falls back to OpenRFPerm and CreateResFile. However, the glue code fails on the 64K ROMs due to the condensed trap table, which is seems to be consistent source of crashes.
To work around this, just check for the 64K ROMs. If so, use SetVol or HSetVol and then call OpenResFile or CreateResFile directly, rather than using the CodeWarrior glue code.
Sixth Crash: I was wrong. StripAddress in glue code
CodeWarrior’s glue code for HOpenResFile needs more than just a check for 64K ROMs. It also calls StripAddress, which wasn’t available until System 4. So, add that check.
Seventh Problem: SetVol nil name is not the same as an empty name
I had difficulty opening a system file on the second drive, because I passed the empty string (“”) instead of nil for the name of the volume in SetVol. This works fine in later systems, but instead resulted in accessing the system file on the first drive in System 1.0. [Editor’s note: I’m not 100% sure on this one, as enough code changed that this needs to be independently verified.]
Eighth Problem: FileFilter paramBlock is garbage
SFGetFile has the option for your code to review all files before displaying them to the user. System 1.0 has a nasty bug where the parameter with the file info actually points to System code, not file data. I spent a day working this out. They fixed this bug in System 1.1. You need to detect that you are using system 1.0 (see ‘STR ’ 0) and just say ‘yes’ to every file, or I guess just provide a limited file type list instead of a filter routine.
Ninth Problem: Finder comments
This has its own write up in the Tiny Transfer manual. In brief, Apple hates file comments. Macintosh Technical Note #29 describes the hash algorithm to get the comment ID on MFS disks from the DeskTop file. What they don’t tell you is that this was not their first choice. Finder 1.0 with System 1.0 generates a random unique ID for the comment resource, and starts the comment with four bytes indicating the file ID. When the user chooses Get Info, Finder 1.0 goes through all the comments looking for the resource that contains the file ID in it. This is a slow algorithm. So, Apple ditched it for the hash algorithm in Finder 1.1g. Apple also changed the resource structure so the comments are straight Pascal strings. They don’t start with file IDs.
Unfortunately, the difference in IDs and resource contents means all Finder 1.0 comments can’t be read by all other Systems. And Finder 1.0 can’t read comments made by all other Systems. Effectively, this was the first of many times when file comments were effectively lost by Apple.
Tenth Problem: Alert with a Button ID Other Than 1
I noticed this in System 6, but it likely affects other versions of the operating system. If you call Alert (NoteAlert, StopAlert, CautionAlert), but the control with ID 1 is not the OK button, then you’ll get a slightly humorous rounded rectangle around the whatever item is ID 1. Simply make the OK button the first ID.
Eleventh Problem: MultiFinder ‘mstr’ Resources
The original Finder provides a file list for an application to open those files when the application starts up. In the original systems, the Finder goes away as the application launches. Because the Finder did not run at the same time as the application, there was no need for a mechanism for the Finder to open files in an already running application.
Starting in System 7, the Finder can open files at any time by sending AppleScript application events to the running application.
That leaves us with the MultiFinder era, circa System 5.x-6.x. Using OS events and short-circuiting certain traps, MultiFinder simulates the user clicking on the application’s File menu, choosing Open, and then picking the file from the standard file dialog in the application. This works well as most applications had this UI pattern. For applications with menus of a different name, they could include ‘mstr’ resource strings that provided the name of the File menu and Open command. Or, they could include ‘mst#’ resource strings with multiple different menu names to try.
Apple’s technical documentation indicates that ‘mstr’ 103 is the name of the Open menu item. However, the documentation doesn’t note that you must also include ‘mstr’ 102 for the name of the File menu. That is, you must include both strings for the mechanism to work.
I know this posting is a massive brain dump. However, I couldn't find most of these answers searching the Internet. Now, hopefully, other people can.
- David
Apple Macintosh System 1 (aka 0.97) released on January 24, 1984. Metrowerks CodeWarrior Gold 11 released on December 31, 1996, which is about 13 years later. By then, the last 68K Macintosh, the PowerBook 190, had been discontinued. For some reason, I thought it would be a good idea to use CodeWarrior 11 to write an application that could run on a Mac 128K with System 1.
Tiny Transfer is my little application for sending files back and forth over a serial cable. Its purpose is to make it easier when repairing, testing, and playing with my old computers. Thus, Tiny Transfer needs to work on all Macs and all Mac OSes.
There are a number of challenges to writing an application for Macintosh System 1.0:
- System 1 officially only runs on the Macintosh 128K, Macintosh 512K and (God forbid) the Macintosh XL. You’ll need to obtain one of these machines and recap it. Or, use Mini vMac with a bespoke build.
- System 1 is officially limited to 400K disks (*cough* not trying to start the debate about 800K floppy drive support). Floppy Emu is a lifesaver here. Not only does it make it easier to move the development app over, but it saves wear and tear on the mechanical floppy drive.
- System 1.0 only contains the calls in Inside Macintosh volume I, II, and III. Oh, wait. Not even that. Page I-4 says Inside Macintosh covers the system dated May 2, which is System 1.1
- It gets worse. Page I-6 explains that the term “[Not in ROM]” does not mean “[On System Disk]”. Believe it or not, many routines were provided by the compiler to build into the application itself.
- There is not a mechanism for detecting unimplemented traps, because no traps yet existed that weren’t implemented. In fact, the unimplemented trap term isn’t defined until late 1986, where is it called ‘UnimplCoreRoutine’ on page V-316. Adding to the difficulty, the trap table is condensed and partially interleaved to save memory. Thus, checking for a nil or missing OS trap will instead find a completely different trap (from the ToolBox) in that parking space. This is by far the biggest problem in developing applications for the Mac 128K and Mac 512K.
- System 1.0 has bugs in both the System and the Finder. Is the fault in your program or the System?
- Only MacsBug 5.x or earlier works [need to verify]. The syntax is different than the 6.x series and the capabilities are limited.
First Crash: Compiler startup code
Metrowerks apparently did not expect System 1 would be targeted by their development environment. I guess System 1 was no longer a commercially large platform in 1996? Compilers insert startup code that runs before your code when your application launches. This startup code initializes global variables and even patches a few traps. Using MacsBug, I determined that a crash was happening before my code even started.
Fortunately, Metrowerks provides the source for the startup code, and they even provide the project file to let you build a custom version. Looking through the source code, we can spot a couple of interesting items.
- CodeWarrior uses a kinda cool custom PackBits routine for compressing/decompressing your global variables and tables. Steve @bigmessowires might enjoy reviewing it, based on his write-up of FC8 compression (https://www.bigmessowires.com/2016/05/06/fc8-faster-68k-decompression/). It is faster and smaller to initialize your global variables inline with their declaration rather than in SetUp routines. The compiler’s startup code then simply loads all the globals at once from a compressed DATA resource.
- CodeWarrior updates the application jump references for your code and flushes the CPU instruction cache. Oh, the 68000 doesn’t have a CPU cache? System 1 doesn’t have a trap to call for flushing? No problem, CodeWarrior checks to make sure the trap exists. But, the 64K ROM’s space-saving table overlaps with a completely different trap, which does exist. CodeWarrior calls the wrong trap. Blam.
- CodeWarrior calls MoveHHi, which doesn’t exist in System 1.
- CodeWarrior patches LoadSegment and UnloadSegment. The Segment traps allow for changing the jump tables as code segments are loaded and moved. But, back then, in the 64K ROMs, you could only patch a trap to the system heap, not the application heap. See Inside Macintosh I-86/87 and II-384
Rather than a major rewrite, I simply check for a 64K ROM after initializing the globals and skip over all the other stuff. This allows it to work on the earliest machines without sacrificing this functionality in later systems. Set the linker to a single segment so the patches to the Segment traps aren’t needed. Add the custom startup lib and set the startup configuration to custom.
I've attached my customized Appl68KStartup.c source file in case anyone else needs it.
Second Crash: MemError() isn’t set
Like any good defensive Macintosh programmer, I call MemError() to check for errors after making a memory manager call. I assumed, wrongly, that the returned ptr or handle is garbage if MemError() reported an error. In that case, I set the pointer to nil. That exercised a piece of code that always assumes there is enough memory for a little allocation at initialization time. There was enough memory, but MemError was a lie. My dereference of a pointer that I set to nil resulted in a crash.
MemError is not actually set by the Memory Manager in the 64K ROMs. Instead, Pascal compiler ‘glue code’ grabs the D0 return value and stuffs it into the MemError global variable, such that the following application call to MemError() works. The CodeWarrior developers weren’t unaware of that (or didn’t bother), as Apple fixed this in the 128K ROMs. See IV-79.
As a developer, you can just just check whether a handle or ptr is not nil when returned. If it is nil, set MemError yourself.
Third Crash: Traps are less tolerant
Usually, I check whether a pointer is nil or a port is open before making a system call with it. I missed one spot where my code asks the serial port if there are incoming bytes. This worked in later systems, because the OS checked the port parameter and noticed it wasn’t open. In System 1, the call just crashes.
The correct fix is to make sure you don’t call the System with a closed port value.
Fourth Crash: WaitNextEvent?
Okay, this actually happened in System 3.3 when I thought using an emulated HD20 would provide more space to exercise my application. The Hard Disk 20 init startup provides a 400K/800K floppy and HD20 driver, along with the hierarchical file system (HFS).
My application started up, showed the menu bar, and then crashed after about 4 seconds with a bomb box. It was system error 28, which says that the heap and stack overlapped. I assumed a loop was filling memory or recursion was happening on a callback.
While stepping through the code with MacsBug, it showed my application calling WaitNextEvent, not GetNextEvent. What? That’s only available with Multifinder or System 7.
Surprise! The OS trap for HFSDispatch (implemented by the Hard Disk 20 init) has the same final digits as WaitNextEvent. Checking the smushed-together 64K ROM trap table wrongly indicates WaitNextEvent is implemented, when it’s actually the HFSDispatch patch. Calling the wrong routine with unexpected parameters every loop eventually fills up memory via the stack.
Macintosh Technical Note #158 provides the solution: WaitNextEvent is never supported on 64K ROMs.
Fifth Crash: HOpenResFile and HCreateResFile
Don’t get fooled by the letter ‘H’ -- these are not HFS traps. That is, they are not part of the HD20 Init or MacPlus ROM that brought HFS to the Macintosh. You need to check for the existence of these traps specifically, not the existence of HFS. CodeWarrior has nice glue code that does this work for you, and falls back to OpenRFPerm and CreateResFile. However, the glue code fails on the 64K ROMs due to the condensed trap table, which is seems to be consistent source of crashes.
To work around this, just check for the 64K ROMs. If so, use SetVol or HSetVol and then call OpenResFile or CreateResFile directly, rather than using the CodeWarrior glue code.
Sixth Crash: I was wrong. StripAddress in glue code
CodeWarrior’s glue code for HOpenResFile needs more than just a check for 64K ROMs. It also calls StripAddress, which wasn’t available until System 4. So, add that check.
Seventh Problem: SetVol nil name is not the same as an empty name
I had difficulty opening a system file on the second drive, because I passed the empty string (“”) instead of nil for the name of the volume in SetVol. This works fine in later systems, but instead resulted in accessing the system file on the first drive in System 1.0. [Editor’s note: I’m not 100% sure on this one, as enough code changed that this needs to be independently verified.]
Eighth Problem: FileFilter paramBlock is garbage
SFGetFile has the option for your code to review all files before displaying them to the user. System 1.0 has a nasty bug where the parameter with the file info actually points to System code, not file data. I spent a day working this out. They fixed this bug in System 1.1. You need to detect that you are using system 1.0 (see ‘STR ’ 0) and just say ‘yes’ to every file, or I guess just provide a limited file type list instead of a filter routine.
Ninth Problem: Finder comments
This has its own write up in the Tiny Transfer manual. In brief, Apple hates file comments. Macintosh Technical Note #29 describes the hash algorithm to get the comment ID on MFS disks from the DeskTop file. What they don’t tell you is that this was not their first choice. Finder 1.0 with System 1.0 generates a random unique ID for the comment resource, and starts the comment with four bytes indicating the file ID. When the user chooses Get Info, Finder 1.0 goes through all the comments looking for the resource that contains the file ID in it. This is a slow algorithm. So, Apple ditched it for the hash algorithm in Finder 1.1g. Apple also changed the resource structure so the comments are straight Pascal strings. They don’t start with file IDs.
Unfortunately, the difference in IDs and resource contents means all Finder 1.0 comments can’t be read by all other Systems. And Finder 1.0 can’t read comments made by all other Systems. Effectively, this was the first of many times when file comments were effectively lost by Apple.
Tenth Problem: Alert with a Button ID Other Than 1
I noticed this in System 6, but it likely affects other versions of the operating system. If you call Alert (NoteAlert, StopAlert, CautionAlert), but the control with ID 1 is not the OK button, then you’ll get a slightly humorous rounded rectangle around the whatever item is ID 1. Simply make the OK button the first ID.
Eleventh Problem: MultiFinder ‘mstr’ Resources
The original Finder provides a file list for an application to open those files when the application starts up. In the original systems, the Finder goes away as the application launches. Because the Finder did not run at the same time as the application, there was no need for a mechanism for the Finder to open files in an already running application.
Starting in System 7, the Finder can open files at any time by sending AppleScript application events to the running application.
That leaves us with the MultiFinder era, circa System 5.x-6.x. Using OS events and short-circuiting certain traps, MultiFinder simulates the user clicking on the application’s File menu, choosing Open, and then picking the file from the standard file dialog in the application. This works well as most applications had this UI pattern. For applications with menus of a different name, they could include ‘mstr’ resource strings that provided the name of the File menu and Open command. Or, they could include ‘mst#’ resource strings with multiple different menu names to try.
Apple’s technical documentation indicates that ‘mstr’ 103 is the name of the Open menu item. However, the documentation doesn’t note that you must also include ‘mstr’ 102 for the name of the File menu. That is, you must include both strings for the mechanism to work.
I know this posting is a massive brain dump. However, I couldn't find most of these answers searching the Internet. Now, hopefully, other people can.
- David