Palm OS Projects 2026-03-29

Building Palm OS Applications on macOS (Apple Silicon)

Palm OS Development

HelloWorld application built on macOS Apple Silicon, running on a Palm Tungsten T3

(Fig. 1: HelloWorld application built on macOS Apple Silicon, running on a Palm Tungsten T3)

Setting up a Palm OS development environment has always been a challenge. Most guides and tools were written for Linux (specifically Ubuntu) or Windows. For macOS users on Apple Silicon (M1/M2/M3/M4/M5), there was no straightforward way to compile Palm OS applications — until now.

This article describes how to set up a complete Palm OS development environment on macOS ARM (Apple Silicon). It is based on savaughn's palmdev-macos toolchain for Palm OS 4 (68K), extended with a dual-target build system, PEAL (Palm Executive ARM Loader), and templates for building native Palm OS 5 (ARM) applications.

If you are looking for a way to build Palm OS applications on Ubuntu 20.04, have a look at this article.

Shortcut

I packed everything needed for building Palm OS applications into a single zip file. No installation, no Homebrew, no sudo required — just unpack and build.

Download the complete toolchain for macOS ARM (Apple Silicon) (~17 MB)

The source of the toolchain is also available on GitHub, as a fork of savaughn's project: github.com/User7142/palmdev-macos

After downloading, unpack the zip file and build the included Hello World example:

unzip 46-file-70bb336d-cf55-4152-9a3e-57bf0cca79ac-palmdev-macos-arm-v1.zip
cd palmdev-macos-arm-v1/helloworld
make build

Output: build/HelloWorld.prc — ready to install on any Palm device or emulator.

macOS Security Note: On first run, macOS may block the toolchain binaries with a "cannot be opened because the developer cannot be verified" warning. To allow them, run this once from the unpacked directory:

xattr -r -d com.apple.quarantine toolchain/ tools/ peal/

What's in the Zip

toolchain/ Cross-compilers: m68k-palmos-gcc (68K), arm-palmos-gcc (ARM), build-prc
sdk/ Palm OS SDK 4.0 and 5r3 (headers and libraries)
peal/ Palm Executive ARM Loader (for native ARM apps on Palm OS 5)
tools/ pilrc — the Palm OS Resource Compiler
templates/ Makefile and source templates for both Palm OS 4 and 5
helloworld/ Working example application, ready to build

Contents of the palmdev-macos-arm ZIP file

(Fig. 2: Contents of the palmdev-macos-arm ZIP file)

Creating Your Own Application

Create a new project directory inside the unpacked toolchain folder and copy the template files:

# Inside the unpacked palmdev-macos-arm-v1/ directory:
mkdir myapp
cd myapp
# Copy the dual-target Makefile and resource template
cp ../templates/Makefile .
cp ../templates/app.rcp .
# For a Palm OS 4 app (pure 68K):
cp ../templates/palmos4/main.c .
make build
# For a Palm OS 5 app (native ARM):
cp ../templates/palmos5/main_arm.c .
cp ../templates/palmos5/stub_m68k.c .
cp ../templates/palmos5/pace.h .
make build TARGET=palmos5

Then edit the Makefile header to set your application name and creator ID:

APP_NAME    = MyApp
CREATOR_ID  = MyAp

The Makefile automatically finds all tools relative to your project directory. Your project must sit one level inside the toolchain folder (e.g., palmdev-macos-arm-v1/myapp/). If you need a different layout, adjust the REPO_DIR variable at the top of the Makefile.

Requirements

  • macOS 11+ (Big Sur or later)
  • Apple Silicon (M1, M2, M3, M4, M5)
  • ~40 MB disk space

That's it. No Homebrew, no Xcode Command Line Tools, no sudo. Everything runs from the unpacked directory.

Optional: To install applications on a real Palm device via USB, you will need pilot-link. This is the only external dependency and only needed for device communication, not for building:

brew tap jichu4n/palm-os
brew trust --formula jichu4n/palm-os/pilot-link
brew install pilot-link

Since Homebrew 6, formulae from third-party taps have to be trusted explicitly, hence the brew trust line. How to build the current pilot-link from source is described in the HotSync article.

Note: These pre-built binaries are for macOS ARM (Apple Silicon) only. For Intel Macs, the existing Homebrew packages already work.

What's in the Toolchain

The zip file contains the following components, all pre-built for macOS ARM:

  1. prc-tools-remix — Pre-built GCC cross-compilers for Palm OS, originally from savaughn's prc-tools-remix:
    • m68k-palmos-gcc (version 2.95.3-kgpd) — compiles C code for Palm OS 4 (68K)
    • arm-palmos-gcc (version 3.3.1) — compiles C code for Palm OS 5 (ARM)
    • build-prc — packages compiled binaries and resources into .prc files
  2. pilrc — The Palm OS Resource Compiler. Turns textual descriptions of forms, menus, and alerts into binary resources.
  3. Palm OS SDK 4.0 and 5r3 — Headers and libraries, from jichu4n/palm-os-sdk
  4. PEAL — Palm Executive ARM Loader for native ARM apps on Palm OS 5, originally from Greg Parker's TCPMP project
  5. peal-postlink — Converts ARM ELF binaries into Palm OS armc resources
  6. Dual-target Makefile template — Build for Palm OS 4 or 5 with a single command
  7. Source templates — Ready-to-use starting points for both 68K and ARM applications

A Note on Creator IDs

Every Palm OS application needs a unique four-character Creator ID (like HlWd or MyAp). This ID was used by Palm OS to associate an application with its databases, preferences, and saved data. Two applications with the same Creator ID would interfere with each other.

Back in the day, Palm Inc. maintained a registration system where developers could reserve their Creator IDs to avoid conflicts. That system no longer exists. For hobby development and retro projects today, just pick something unique and avoid well-known IDs like memo, addr, or date (which belong to the built-in applications). A good approach is to use a mix of upper and lowercase letters that is unlikely to collide, for example derived from your app name.

Building a Palm OS 4 Application (68K)

Building a Palm OS 4 application is straightforward. The included HelloWorld example demonstrates the basics. Let's look at the source code first.

HelloWorld.c

#include <PalmOS.h>
UInt32 PilotMain( UInt16 cmd, void *cmdPBP, UInt16 launchFlags )
{
    EventType event;
    if (cmd == sysAppLaunchCmdNormalLaunch) {
        //  Display a string.
        WinDrawChars( "Hello, world!", 13, 55, 60 );
        //  Main event loop:
        do {
            //  Doze until an event arrives.
            EvtGetEvent( &event, evtWaitForever );
            //  System gets first chance to handle the event.
            SysHandleEvent( &event );
        } while (event.eType != appStopEvent);
    }
    return 0;
}

Every Palm OS application has a PilotMain() function as its entry point — similar to main() in standard C, but with Palm OS-specific parameters. The event loop is the heart of every Palm OS application: it waits for events, lets the system handle them, and exits when the user closes the app.

HelloWorld.rcp

/*
 * HelloWorld Resource File
 */
#define MainForm 1000
FORM ID MainForm AT (0 0 160 160)
USABLE
BEGIN
    TITLE "HelloWorld"
END

The .rcp file defines the application's user interface resources. PilRC (PILot Resource Compiler) compiles this textual description into binary resources that are included in the final .prc file. You define forms, buttons, labels, menus, alerts, and bitmaps here.

Building

cd helloworld
make build

The build process is:

  1. m68k-palmos-gcc compiles .c sources to .o object files
  2. m68k-palmos-gcc links the object files into an ELF binary
  3. pilrc compiles the .rcp file into .bin resources
  4. build-prc packages the ELF binary and resources into the final .prc file

Output: build/HelloWorld.prc — about 945 bytes, ready for any Palm device.

Terminal output of make build for Palm OS 4 (68K)

(Fig. 3: Terminal output of make build for Palm OS 4 (68K))

The Makefile

The key to making this work without modifying system files is the -B flag. It tells GCC exactly where to find the assembler, linker, and libraries — locally, without needing sudo or system-wide installation:

CC = $(TOOLCHAIN_DIR)/bin/m68k-palmos-gcc
CFLAGS = -B$(TOOLCHAIN_DIR)/lib/gcc-lib/m68k-palmos/2.95.3-kgpd/ \
         -B$(TOOLCHAIN_DIR)/m68k-palmos/bin/ \
         -B$(TOOLCHAIN_DIR)/m68k-palmos/lib/ \
         -nostdinc \
         -I$(SDK_DIR)/include \
         -I$(SDK_DIR)/include/Core \
         -I$(SDK_DIR)/include/Core/Hardware \
         -I$(SDK_DIR)/include/Core/System \
         -I$(SDK_DIR)/include/Core/UI \
         -I$(SDK_DIR)/include/Dynamic \
         -I$(SDK_DIR)/include/Libraries \
         -O2 -Wall

The -nostdinc flag is critical: it prevents macOS system headers from conflicting with the Palm OS headers. Without it, you will get strange compilation errors because the compiler tries to mix modern macOS headers with 20-year-old Palm OS definitions.

You will also notice GCC_EXEC_PREFIX being set in the Makefile:

export GCC_EXEC_PREFIX = $(TOOLCHAIN_DIR)/lib/gcc-lib/

This environment variable tells GCC where to find its internal support files (specs, libraries, compiler passes). Without it, GCC would look in the default system paths and fail because the Palm OS cross-compiler files are installed locally, not system-wide.

Building a Palm OS 5 Application (ARM)

This is where it gets interesting. Palm OS 5 devices (like the Tungsten T3 or Treo 650) have ARM processors, but Palm OS itself still runs on a 68K emulator called PACE (Palm Application Compatibility Environment). To run native ARM code, you need a special loader that bridges the two architectures.

That loader is PEAL — the Palm Executive ARM Loader, originally developed by Greg Parker for the TCPMP media player project.

How ARM Apps Work on Palm OS 5

A Palm OS 5 ARM application consists of three layers:

  1. 68K Stub — The entry point that Palm OS loads. It detects the ARM processor, loads the ARM binary via PEAL, and transfers control to it.
  2. ARM Binary — Your actual application, compiled as native ARM code and packed into armc resources by the peal-postlink tool.
  3. PACE Wrappers — Since ARM code cannot directly call Palm OS API traps (which are 68K instructions), every API call goes through a wrapper that calls back into the 68K emulator.

The performance benefit is significant: native ARM code runs roughly 10x faster than the same code emulated through the 68K layer. This is essential for applications that need real-time performance, like audio synthesizers or games.

Creating a Palm OS 5 Project Step by Step

Here is a complete walkthrough for creating a new Palm OS 5 application from scratch. We assume you have unpacked the zip file and are inside the toolchain directory.

Step 1: Create a project directory inside the toolchain folder:

mkdir myapp
cd myapp

Step 2: Copy the template files:

# Dual-target Makefile (supports both Palm OS 4 and 5)
cp ../templates/Makefile .
# Resource file (UI definitions)
cp ../templates/app.rcp .
# Palm OS 5 ARM source files
cp ../templates/palmos5/main_arm.c .
cp ../templates/palmos5/stub_m68k.c .
cp ../templates/palmos5/pace.h .

Step 3: Edit the Makefile header to configure your application:

APP_NAME    = MyApp
CREATOR_ID  = MyAp
RESOURCES   = app.rcp
ARM_SOURCES = main_arm.c

The Makefile auto-detects the toolchain path relative to your project directory. It assumes your project is one level inside the toolchain folder (e.g., palmdev-macos-arm-v1/myapp/). If you place your project elsewhere, adjust the REPO_DIR variable at the top of the Makefile.

Step 4: Check that your app.rcp contains an alert resource at ID 1100. The 68K stub uses FrmCustomAlert(1100, ...) to display error messages (e.g., when no ARM processor is found). The template already includes this:

#define AboutAlert  1100
ALERT ID AboutAlert
INFORMATION
BEGIN
    TITLE "Info"
    MESSAGE "^1"
    BUTTONS "OK"
END

The ^1 placeholder is replaced at runtime by the actual error message passed to FrmCustomAlert(). If you forget this alert, the application will crash when trying to show error messages on non-ARM devices.

Step 5: Build:

make build TARGET=palmos5

The output is build/palmos5/MyApp.prc — a single file containing the 68K stub, the PEAL loader, and the native ARM binary.

Terminal output of make build TARGET=palmos5 for Palm OS 5 (ARM)

(Fig. 4: Terminal output of make build TARGET=palmos5 for Palm OS 5 (ARM))

The 68K Stub

The stub is compiled with m68k-palmos-gcc and serves as the gateway between Palm OS and your ARM code:

#include <PalmOS.h>
#include "peal.h"
UInt32 PilotMain(UInt16 cmd, void *cmdPBP, UInt16 launchFlags)
{
    PealModule *m;
    void *entry;
    UInt32 result;
    UInt32 cpuType;
    if (cmd != sysAppLaunchCmdNormalLaunch)
        return errNone;
    /* Check if ARM CPU is present */
    FtrGet(sysFileCSystem, sysFtrNumProcessorID, &cpuType);
    if (cpuType == sysFtrNumProcessorx86) {
        FrmCustomAlert(1100, "Simulator not supported.", "", "");
        return errNone;
    }
    if (!sysFtrNumProcessorIsARM(cpuType)) {
        FrmCustomAlert(1100, "ARM processor required.", "", "");
        return errNone;
    }
    /* Load ARM code from 'armc' resources (ID 1000+) */
    m = PealLoadFromResources('armc', 1000, NULL, 0, 0, 0, 0, 0);
    if (!m) {
        FrmCustomAlert(1100, "Could not load ARM code.", "", "");
        return errNone;
    }
    /* Find ARM entry point */
    entry = PealLookupSymbol(m, "AppMain");
    if (!entry) {
        PealUnload(m);
        FrmCustomAlert(1100, "AppMain not found.", "", "");
        return errNone;
    }
    /* Execute ARM code */
    result = PealCall(m, entry, cmdPBP);
    PealUnload(m);
    return result;
}

The stub first checks if an ARM processor is available (gracefully failing on simulators or older 68K-only devices), then uses PEAL to load the ARM binary from armc resources, looks up the AppMain symbol, and calls it.

The PEAL headers (peal.h) and the PEAL loader source (peal.c) are located in the peal/m68k/ directory of the repository. The Makefile automatically finds and compiles them — you do not need to copy them into your project.

The ARM Application

The ARM main application looks similar to a regular Palm OS app, but with one crucial difference: every Palm OS API call must go through a PACE wrapper that handles the byte-swapping (ARM is little-endian, 68K is big-endian) and the transition back to the 68K emulator:

#include <PalmOS.h>
#include <PceNativeCall.h>
#include "pealstub.h"
#include "pace.h"
#define MainForm 1000
static Boolean MainFormHandleEvent(void *eventP)
{
    UInt16 eType;
    void *form;
    eType = ByteSwap16(*(UInt16 *)eventP);
    switch (eType) {
    case 0x1601: /* frmOpenEvent */
        form = pace_FrmGetActiveForm();
        pace_FrmDrawForm(form);
        return 1;
    default:
        break;
    }
    return 0;
}
static void AppEventLoop(void)
{
    UInt8 event[64];
    UInt16 eType;
    do {
        pace_EvtGetEvent(event, (Int32)0x7FFFFFFF);
        eType = ByteSwap16(*(UInt16 *)event);
        if (!pace_SysHandleEvent(event)) {
            UInt16 err = 0;
            if (!pace_MenuHandleEvent(0, event, &err)) {
                if (eType == 0x1600) { /* frmLoadEvent */
                    UInt16 formID = ByteSwap16(
                        *(UInt16 *)((UInt8 *)event + 8));
                    void *form = pace_FrmInitForm(formID);
                    pace_FrmSetActiveForm(form);
                }
                if (eType == 0x1601) { /* frmOpenEvent */
                    MainFormHandleEvent(event);
                } else {
                    pace_FrmDispatchEvent(event);
                }
            }
        }
    } while (eType != 0x16FF); /* appStopEvent */
}
unsigned long AppMain(void *cmdPBP)
{
    pace_FrmGotoForm(MainForm);
    AppEventLoop();
    pace_FrmCloseAllForms();
    return 0;
}

Notice how every Palm OS function is prefixed with pace_. These are inline wrapper functions defined in pace.h that handle the byte-swapping and the call into the 68K emulator.

Understanding PACE Wrappers

The included pace.h provides wrappers for the most commonly used Palm OS APIs: event handling, form management, alerts, drawing, memory allocation, and audio streaming. For example, pace_EvtGetEvent() looks like this:

static inline void pace_EvtGetEvent(void *eventP, Int32 timeout) {
    UInt8 args[8];
    *(UInt32 *)&args[0] = ByteSwap32((UInt32)eventP);
    *(Int32  *)&args[4] = (Int32)ByteSwap32((UInt32)timeout);
    PaceCall(PceNativeTrapNo(sysTrapEvtGetEvent), args, 8);
}

The arguments are packed into a byte array in big-endian format, and the call goes through PceNativeTrapNo() to invoke the 68K trap via the emulator. The pattern is always the same:

  1. Create a byte buffer large enough for all arguments
  2. Pack each argument in big-endian order using ByteSwap16() or ByteSwap32()
  3. Call PaceCall() (returns UInt32) or PaceCallPtr() (returns a pointer) with the trap number, the argument buffer, and its size in bytes

If you need a Palm OS API that is not in pace.h, you can write your own wrapper following this pattern. Look up the function signature in the SDK headers (e.g., sdk/sdk-5r3/include/Core/System/), then:

/* Example: wrapping DmNumDatabases(cardNo) -> UInt16 */
static inline UInt16 pace_DmNumDatabases(UInt16 cardNo) {
    UInt16 arg = ByteSwap16(cardNo);
    return (UInt16)PaceCall(PceNativeTrapNo(sysTrapDmNumDatabases),
                            &arg, 2);
}
/* Example: wrapping a function that returns a pointer */
static inline void *pace_DmGetDatabase(UInt16 cardNo, UInt16 index) {
    UInt8 args[4];
    *(UInt16 *)&args[0] = ByteSwap16(cardNo);
    *(UInt16 *)&args[2] = ByteSwap16(index);
    return PaceCallPtr(PceNativeTrapNo(sysTrapDmGetDatabase), args, 4);
}

The trap names (sysTrap...) are defined in the SDK header Core/System/SysTraps.h. Use PaceCall() for functions that return integers or void, and PaceCallPtr() for functions that return pointers.

The 7-Step Build Pipeline

Building a Palm OS 5 application requires a more complex pipeline than Palm OS 4. The dual-target Makefile handles all of this with a single command:

make build TARGET=palmos5

Behind the scenes, seven steps are executed:

  1. ARM compilation: arm-palmos-gcc compiles your .c source files into ARM .o object files, with -march=armv4 -mtune=xscale optimization flags.
  2. ARM linking: arm-palmos-gcc links the object files into an ARM ELF binary. pealstub.o must be linked first! The linker uses --emit-relocs (for runtime relocation) and --split-by-file=63000 (to manage section sizes within Palm OS resource limits).
  3. Postlinking: peal-postlink converts the ARM ELF binary into a .ro file containing armc resources. This is the step that makes ARM code loadable by Palm OS.
  4. 68K compilation: m68k-palmos-gcc compiles the 68K stub and the PEAL loader (from peal/m68k/peal.c).
  5. 68K linking: m68k-palmos-gcc links the stub and loader into a 68K ELF binary.
  6. Resource compilation: pilrc compiles the .rcp file into .bin resources.
  7. PRC assembly: build-prc packages the 68K ELF, .bin resources, and .ro ARM resources into the final .prc file.

The result is a single .prc file that contains both the 68K stub and the native ARM binary.

The Dual-Target Makefile

One of the nicest features of this toolchain is the dual-target Makefile template. The same Makefile supports both Palm OS 4 and Palm OS 5 builds, selected by the TARGET variable:

# Build for Palm OS 4 (pure 68K) - this is the default
make build
# Build for Palm OS 5 (ARM native with 68K stub)
make build TARGET=palmos5
# Clean build artifacts
make clean
# Clean and rebuild
make rebuild

The Makefile auto-detects the toolchain location relative to your project directory:

# Auto-detect: project sits inside the toolchain folder (e.g. palmdev-macos-arm-v1/myapp/)
REPO_DIR      := $(shell cd .. && pwd)
TOOLCHAIN_DIR = $(REPO_DIR)/toolchain
PEAL_DIR      = $(REPO_DIR)/peal
SDK4_DIR      = $(REPO_DIR)/sdk/sdk-4
SDK5_DIR      = $(REPO_DIR)/sdk/sdk-5r3

This means your project directory must be one level inside the unpacked toolchain folder. For example: ~/palmdev-macos-arm-v1/myapp/. If you need a different layout, just change REPO_DIR to point to the toolchain root.

peal-postlink: The Bridge Between ARM and Palm OS

The peal-postlink tool is a critical piece of the Palm OS 5 build pipeline. It takes an ARM ELF binary (the output of arm-palmos-gcc) and converts it into Palm OS armc resources that can be loaded at runtime by the PEAL loader.

It performs several operations:

  • Parses ELF headers, program headers, and section headers
  • Extracts code and read-only data sections
  • Strips the symbol table (keeping only global symbols needed for lookups, like AppMain)
  • Processes relocations, converting absolute addresses to position-independent form
  • Generates a Global Offset Table (GOT) for position-independent code
  • Outputs the result as Palm OS resource records (starting at ID 1000)

The tool is written in about 2,800 lines of C++ and is included as a pre-built binary for macOS ARM. The source code is available in the peal/postlink/ directory if you need to rebuild it:

c++ -O2 -W -Wall image.cc postlinker.cc relocation.cc \
    section.cc symbol.cc symboltable.cc complain.cc -o peal-postlink

Testing Your Application

On a Real Palm Device

Install the .prc file via USB with pilot-link. Start the command first, then press the HotSync button on the Palm:

pilot-xfer -p usb: -i build/HelloWorld.prc

More comfortable: my HotSync app for macOS sits in the menu bar and installs every .prc file you double-click on the next HotSync.

Using POSE (Palm OS Emulator)

POSE is a 32-bit application and does not run natively on macOS. However, it can be run on macOS (including Apple Silicon) via Docker. For a detailed guide on setting this up, see Running POSE on macOS with Docker.

Once POSE is running, you can drag and drop the .prc file onto the emulator window to install it. Note that POSE emulates a 68K processor only — Palm OS 5 ARM applications will show the "ARM processor required" error message in POSE, which is expected. To test ARM applications, you need a real Palm OS 5 device or an emulator that supports ARM, like the Palm OS Simulator (which requires a separate setup).

Troubleshooting

"cannot be opened because the developer cannot be verified"

macOS quarantines downloaded binaries by default. Remove the quarantine flag from the toolchain:

xattr -r -d com.apple.quarantine toolchain/ tools/ peal/

Compilation errors with system headers

Make sure the -nostdinc flag is included in your CFLAGS (it is in the template Makefile). This prevents macOS system headers from conflicting with the Palm OS headers. Without it, the compiler mixes modern macOS types with Palm OS types and produces confusing errors.

Linker errors about missing libraries

Check that GCC_EXEC_PREFIX is set correctly in your Makefile. This environment variable tells GCC where to find its internal support files. The template Makefile sets it automatically:

export GCC_EXEC_PREFIX = $(TOOLCHAIN_DIR)/lib/gcc-lib/

"pilrc: command not found" or wrong pilrc path

The Makefile looks for pilrc in three locations, in order: tools/pilrc (bundled in the zip), /opt/homebrew/opt/pilrc/bin/pilrc (Homebrew), and finally in your PATH. If you moved the toolchain or pilrc is elsewhere, set the PILRC variable in your Makefile manually.

Credits

This toolchain builds on the work of many people in the Palm OS community:

  • savaughn — prc-tools macOS ARM builds and the palmdev-macos project
  • jichu4n — Palm OS SDK packaging, Homebrew tap for pilrc and pilot-link
  • Greg Parker — PEAL (Palm Executive ARM Loader), originally from the TCPMP project
  • The prc-tools community — Maintaining the GCC toolchain for Palm OS since the 1990s
  • Palm, Inc. — The original Palm OS SDK and documentation

The fact that we can still build native applications for devices from the early 2000s, on a modern Apple Silicon Mac, is a testament to the durability of open-source tools and the dedication of the retro computing community.

Comments on "Building Palm OS Applications on macOS (Apple Silicon)"

Comments are moderated before publication. Only substantive and constructive comments will be approved.

Markdown: **bold**, *italic*, `code`, [link](url) /5000
Drop images here or browse
Replying to
Drop images or browse