forked from wezm/wezm.net
451 lines
15 KiB
Markdown
451 lines
15 KiB
Markdown
+++
|
||
title = "Trying to Run Rust on Classic Mac OS"
|
||
date = 2023-02-27T10:06:28+10:00
|
||
|
||
#[extra]
|
||
#updated = 2023-01-11T21:11:28+10:00
|
||
+++
|
||
|
||
I recently acquired a Power Macintosh 9500/150 and after cleaning it up and
|
||
building a [BlueSCSI] to replace the failed hard drive it's now in a
|
||
semi-operational state. This weekend I thought I'd see if I could build a Mac
|
||
app for it that called some Rust code. This post details my trials and
|
||
tribulations.
|
||
|
||
<!-- more -->
|
||
|
||
I started by building [Retro68], which is a modernish GCC based toolchain
|
||
that allows cross-compiling applications for 68K and PPC Macs. With Retro68
|
||
built I set up a VM in [SheepShaver] running Mac OS 8.1. Using the LaunchAAPL
|
||
and LaunchAAPLServer tools that come with Retro68 I was able to build the
|
||
sample applications and launch them in the emulated Mac.
|
||
|
||
With the basic workflow working I set about creating a Rust project that built
|
||
a static library with one very basic exported function. It just returns a
|
||
static [Pascal string] when called.
|
||
|
||
```rust
|
||
#![no_std]
|
||
#![feature(lang_items)]
|
||
|
||
use core::panic::PanicInfo;
|
||
|
||
static MSG: &[u8] = b"\x04Rust";
|
||
|
||
#[no_mangle]
|
||
pub unsafe extern "C" fn hello_rust() -> *const u8 {
|
||
MSG.as_ptr()
|
||
}
|
||
|
||
#[panic_handler]
|
||
fn panic(_panic: &PanicInfo<'_>) -> ! {
|
||
loop {}
|
||
}
|
||
|
||
#[lang = "eh_personality"]
|
||
extern "C" fn eh_personality() {}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_msg_is_pascal_string() {
|
||
assert_eq!(MSG[0], MSG[1..].len().try_into().unwrap());
|
||
}
|
||
}
|
||
```
|
||
|
||
Classic Mac OS is not a target that Rust knows about so I created a custom
|
||
target JSON definition named `powerpc-apple-macos.json` based on prior work by
|
||
kmeisthax in [this GitHub discussion](https://github.com/autc04/Retro68/discussions/123#discussioncomment-597268):
|
||
|
||
```json
|
||
{
|
||
"arch": "powerpc",
|
||
"data-layout": "E-m:a-p:32:32-i64:64-n32",
|
||
"executables": true,
|
||
"llvm-target": "powerpc-unknown-none",
|
||
"max-atomic-width": 32,
|
||
"os": "macosclassic",
|
||
"vendor": "apple",
|
||
"target-endian": "big",
|
||
"target-pointer-width": "32",
|
||
"linker": "powerpc-apple-macos-gcc",
|
||
"linker-flavor": "gcc",
|
||
"linker-is-gnu": true
|
||
}
|
||
```
|
||
|
||
I was able to build the static library with this cargo invocation:
|
||
|
||
```
|
||
cargo +nightly build --release -Z build-std=core --target powerpc-apple-macos.json
|
||
```
|
||
|
||
It's using nightly because it's using unstable features to build `core` and the
|
||
`eh_personality` lang item in the code.
|
||
|
||
This successfully compiles and produces
|
||
`target/powerpc-apple-macos/release/libclassic_mac_rust.a`
|
||
|
||
I used the [Dialog sample] from Retro68 as the basis of my Mac app. Here it is
|
||
running prior to Rust integration:
|
||
|
||
{{ figure(image="posts/2023/rust-on-ppc-classic-mac-os/dialog-sample.png", link="posts/2023/rust-on-ppc-classic-mac-os/dialog-sample.png", alt="Screenshot of SheepShaver running Mac OS 8.1. It shows some Finder windows with a frontmost dialog that has a text label, text field, radio buttons, check box and Quit button.", caption="Dialog Sample") }}
|
||
|
||
This is my tweaked version of the C file:
|
||
|
||
```c
|
||
/*
|
||
Copyright 2015 Wolfgang Thaller.
|
||
|
||
This file is part of Retro68.
|
||
|
||
Retro68 is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
Retro68 is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with Retro68. If not, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#include <Quickdraw.h>
|
||
#include <Dialogs.h>
|
||
#include <Fonts.h>
|
||
|
||
#ifndef TARGET_API_MAC_CARBON
|
||
/* NOTE: this is checking whether the Dialogs.h we use *knows* about Carbon,
|
||
not whether we are actually compiling for Cabon.
|
||
If Dialogs.h is older, we add a define to be able to use the new name
|
||
for NewUserItemUPP, which used to be NewUserItemProc. */
|
||
|
||
#define NewUserItemUPP NewUserItemProc
|
||
#endif
|
||
|
||
extern ConstStringPtr hello_rust(void);
|
||
|
||
pascal void ButtonFrameProc(DialogRef dlg, DialogItemIndex itemNo)
|
||
{
|
||
DialogItemType type;
|
||
Handle itemH;
|
||
Rect box;
|
||
|
||
GetDialogItem(dlg, 1, &type, &itemH, &box);
|
||
InsetRect(&box, -4, -4);
|
||
PenSize(3,3);
|
||
FrameRoundRect(&box,16,16);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
#if !TARGET_API_MAC_CARBON
|
||
InitGraf(&qd.thePort);
|
||
InitFonts();
|
||
InitWindows();
|
||
InitMenus();
|
||
TEInit();
|
||
InitDialogs(NULL);
|
||
#endif
|
||
DialogPtr dlg = GetNewDialog(128,0,(WindowPtr)-1);
|
||
InitCursor();
|
||
SelectDialogItemText(dlg,4,0,32767);
|
||
|
||
ConstStr255Param param1 = hello_rust();
|
||
|
||
ParamText(param1, "\p", "\p", "\p");
|
||
|
||
DialogItemType type;
|
||
Handle itemH;
|
||
Rect box;
|
||
|
||
GetDialogItem(dlg, 2, &type, &itemH, &box);
|
||
SetDialogItem(dlg, 2, type, (Handle) NewUserItemUPP(&ButtonFrameProc), &box);
|
||
|
||
ControlHandle cb, radio1, radio2;
|
||
GetDialogItem(dlg, 5, &type, &itemH, &box);
|
||
cb = (ControlHandle)itemH;
|
||
GetDialogItem(dlg, 6, &type, &itemH, &box);
|
||
radio1 = (ControlHandle)itemH;
|
||
GetDialogItem(dlg, 7, &type, &itemH, &box);
|
||
radio2 = (ControlHandle)itemH;
|
||
SetControlValue(radio1, 1);
|
||
|
||
short item;
|
||
do {
|
||
ModalDialog(NULL, &item);
|
||
|
||
if(item >= 5 && item <= 7)
|
||
{
|
||
if(item == 5)
|
||
SetControlValue(cb, !GetControlValue(cb));
|
||
if(item == 6 || item == 7)
|
||
{
|
||
SetControlValue(radio1, item == 6);
|
||
SetControlValue(radio2, item == 7);
|
||
}
|
||
}
|
||
} while(item != 1);
|
||
|
||
FlushEvents(everyEvent, -1);
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
And this is the resource file (`dialog.r`):
|
||
|
||
```
|
||
/*
|
||
Copyright 2015 Wolfgang Thaller.
|
||
|
||
This file is part of Retro68.
|
||
|
||
Retro68 is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
Retro68 is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with Retro68. If not, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#include "Dialogs.r"
|
||
|
||
resource 'DLOG' (128) {
|
||
{ 50, 100, 240, 420 },
|
||
dBoxProc,
|
||
visible,
|
||
noGoAway,
|
||
0,
|
||
128,
|
||
"",
|
||
centerMainScreen
|
||
};
|
||
|
||
resource 'DITL' (128) {
|
||
{
|
||
{ 190-10-20, 320-10-80, 190-10, 320-10 },
|
||
Button { enabled, "Quit" };
|
||
|
||
{ 190-10-20-5, 320-10-80-5, 190-10+5, 320-10+5 },
|
||
UserItem { enabled };
|
||
|
||
{ 10, 10, 30, 310 },
|
||
StaticText { enabled, "Hello ^0" };
|
||
|
||
{ 40, 10, 56, 310 },
|
||
EditText { enabled, "Edit Text Item" };
|
||
|
||
{ 70, 10, 86, 310 },
|
||
CheckBox { enabled, "Check Box" };
|
||
|
||
{ 90, 10, 106, 310 },
|
||
RadioButton { enabled, "Radio 1" };
|
||
|
||
{ 110, 10, 126, 310 },
|
||
RadioButton { enabled, "Radio 2" };
|
||
}
|
||
};
|
||
|
||
#include "Processes.r"
|
||
|
||
resource 'SIZE' (-1) {
|
||
reserved,
|
||
acceptSuspendResumeEvents,
|
||
reserved,
|
||
canBackground,
|
||
doesActivateOnFGSwitch,
|
||
backgroundAndForeground,
|
||
dontGetFrontClicks,
|
||
ignoreChildDiedEvents,
|
||
is32BitCompatible,
|
||
#ifdef TARGET_API_MAC_CARBON
|
||
isHighLevelEventAware,
|
||
#else
|
||
notHighLevelEventAware,
|
||
#endif
|
||
onlyLocalHLEvents,
|
||
notStationeryAware,
|
||
dontUseTextEditServices,
|
||
reserved,
|
||
reserved,
|
||
reserved,
|
||
#ifdef TARGET_API_MAC_CARBON
|
||
500 * 1024, // Carbon apparently needs additional memory.
|
||
500 * 1024
|
||
#else
|
||
100 * 1024,
|
||
100 * 1024
|
||
#endif
|
||
};
|
||
```
|
||
|
||
The main differences are:
|
||
|
||
* `extern` declaration for the Rust function
|
||
* Using the string returned from `hello_rust` to set `ParamText`
|
||
* Changing the StaticText control's text to "Hello ^0" in order to make use of
|
||
the `ParamText`
|
||
* Adding `target_link_libraries(Dialog ${CMAKE_SOURCE_DIR}/target/powerpc-apple-macos/release/libclassic_mac_rust.a)`
|
||
to `CMakeLists.txt` to have CMake link with the Rust library.
|
||
|
||
{{ figure(image="posts/2023/rust-on-ppc-classic-mac-os/ParamText.jpg", link="posts/2023/rust-on-ppc-classic-mac-os/ParamText.jpg", alt="Photo of ParamText documentation from my copy of Inside Macintosh Volume Ⅰ", caption="ParamText documentation from my copy of Inside Macintosh Volume Ⅰ") }}
|
||
|
||
Now when building the project we get…
|
||
|
||
```
|
||
ninja: Entering directory `cmake-build-retro68ppc'
|
||
[1/4] Linking C executable Dialog.xcoff
|
||
FAILED: Dialog.xcoff
|
||
: && /home/wmoore/Source/github.com/autc04/Retro68-build/toolchain/bin/powerpc-apple-macos-gcc -Wl,-gc-sections CMakeFiles/Dialog.dir/dialog.obj -o Dialog.xcoff /home/wmoore/Projects/classic-mac-rust/target/powerpc-apple-macos/release/libclassic_mac_rust.a && :
|
||
/home/wmoore/Source/github.com/autc04/Retro68-build/toolchain/lib/gcc/powerpc-apple-macos/9.1.0/../../../../powerpc-apple-macos/bin/ld:/home/wmoore/Projects/classic-mac-rust/target/powerpc-apple-macos/release/libclassic_mac_rust.a: file format not recognized; treating as linker script
|
||
/home/wmoore/Source/github.com/autc04/Retro68-build/toolchain/lib/gcc/powerpc-apple-macos/9.1.0/../../../../powerpc-apple-macos/bin/ld:/home/wmoore/Projects/classic-mac-rust/target/powerpc-apple-macos/release/libclassic_mac_rust.a:1: syntax error
|
||
collect2: error: ld returned 1 exit status
|
||
ninja: build stopped: subcommand failed.
|
||
```
|
||
|
||
It doesn't like `libclassic_mac_rust.a`. Some investigation shows that the objects in the library
|
||
are in ELF format. `powerpc-apple-macos-objcopy --info` shows that Retro68 does not handle
|
||
ELF:
|
||
|
||
```
|
||
BFD header file version (GNU Binutils) 2.31.1
|
||
xcoff-powermac
|
||
(header big endian, data big endian)
|
||
powerpc:common
|
||
rs6000:6000
|
||
srec
|
||
(header endianness unknown, data endianness unknown)
|
||
powerpc:common
|
||
rs6000:6000
|
||
symbolsrec
|
||
(header endianness unknown, data endianness unknown)
|
||
powerpc:common
|
||
rs6000:6000
|
||
verilog
|
||
(header endianness unknown, data endianness unknown)
|
||
powerpc:common
|
||
rs6000:6000
|
||
tekhex
|
||
(header endianness unknown, data endianness unknown)
|
||
powerpc:common
|
||
rs6000:6000
|
||
binary
|
||
(header endianness unknown, data endianness unknown)
|
||
powerpc:common
|
||
rs6000:6000
|
||
ihex
|
||
(header endianness unknown, data endianness unknown)
|
||
powerpc:common
|
||
rs6000:6000
|
||
|
||
xcoff-powermac srec symbolsrec verilog tekhex binary ihex
|
||
powerpc:common xcoff-powermac srec symbolsrec verilog tekhex binary ihex
|
||
rs6000:6000 xcoff-powermac srec symbolsrec verilog tekhex binary ihex
|
||
```
|
||
|
||
It looks like it really only supports `xcoff-powermac`, which was derived from
|
||
rs6000 AIX. At this point I tried to find a way to convert my ELF objects to
|
||
XCOFF. I eventually stumbled across
|
||
[this thread on the Haiku forum](https://discuss.haiku-os.org/t/xcoff-pef/12445/15)
|
||
that mentions that `powerpc-linux-gnu-binutils` on Debian knows about
|
||
`aixcoff-rs6000`. So I fired up a Debian docker container and tried converting
|
||
my `.a`, and it worked:
|
||
|
||
```
|
||
docker run --rm -it -v $(pwd):/src debian:testing
|
||
apt update
|
||
apt install powerpc-linux-gnu-binutils
|
||
powerpc-linux-gnu-objcopy -O aixcoff-rs6000 /src/target/powerpc-apple-macos/release/libclassic_mac_rust.a /src/target/powerpc-apple-macos/release/libclassic_mac_rust.obj
|
||
```
|
||
|
||
Examining the objects in the new archive showed that they were now in the same
|
||
format as the objects generated by Retro68. I updated the `CMakeLists.txt` to
|
||
point at the new library and tried building again:
|
||
|
||
```
|
||
/home/wmoore/Source/github.com/autc04/Retro68-build/toolchain/lib/gcc/powerpc-apple-macos/9.1.0/../../../../powerpc-apple-macos/bin/ld: /home/wmoore/Projects/classic-mac-rust/target/powerpc-apple-macos/release/libclassic_mac_rust.obj(classic_mac_rust-80e61781bab75910.classic_mac_rust.9ba2ce33-cgu.0.rcgu.o): class 2 symbol `hello_rust' has no aux entries
|
||
```
|
||
|
||
Now we get further. It can read the `.a` now and even sees the `hello_rust`
|
||
symbol but it
|
||
[looks like it's looking for an aux entry to determine the symbol type](https://github.com/autc04/Retro68/blob/5f882506013a0a8a4335350197a1b7c91763494e/binutils/bfd/xcofflink.c#L1461-L1478)
|
||
but not finding one. AUX entries are an
|
||
[XCOFF](https://www.ibm.com/docs/en/aix/7.2?topic=formats-xcoff-object-file-format)
|
||
thing.
|
||
|
||
One other thing I tried was setting the `llvm-target` in the custom target JSON
|
||
to `powerpc-ibm-aix`. Due to the heritage of PPC Mac OS the ABI is the same
|
||
(Apple used the AIX toolchain, which is why object files use XCOFF even though
|
||
executables use PEF). This target would be ideal as it would use the right ABI
|
||
and emit XCOFF by default.
|
||
|
||
Unfortunately it runs into unimplemented parts of LLVM's XCOFF implementation:
|
||
|
||
> LLVM ERROR: relocation for paired relocatable term is not yet supported
|
||
|
||
Rust uses a fork/snapshot of LLVM but the
|
||
[issue is still present in LLVM master](https://github.com/rust-lang/llvm-project/blob/5ef9f9948fca7cb39dd6c1935ca4e819fb7a0db2/llvm/lib/MC/XCOFFObjectWriter.cpp).
|
||
[This post on writing a Mac OS 9 application in Swift][swift] goes down a
|
||
similar path using the AIX target and also mentions patching the Swift compiler
|
||
to avoid the unsupported parts of LLVMs XCOFF implementation. That's an avenue
|
||
for future experimentation.
|
||
|
||
### rustc\_codegen\_gcc
|
||
|
||
At this point I decided to try a different approach.
|
||
[rustc\_codegen\_gcc](https://github.com/rust-lang/rustc_codegen_gcc) is a
|
||
codegen plugin that uses [libgccjit] for code generation instead of LLVM. The
|
||
motivation of the project is promising for my use case:
|
||
|
||
> The primary goal of this project is to be able to compile Rust code on
|
||
> platforms unsupported by LLVM.
|
||
|
||
I found the instructions for using `rustc_codegen_gcc` a bit difficult to
|
||
follow, especially when trying to build a cross-compiler.
|
||
|
||
I eventually managed to rebuild Retro68 with `libgccjit` enabled and then coax
|
||
`rustc_codegen_gcc` to use it. Unsurprisingly that quickly failed as Retro68 is
|
||
based on GCC 9.1 and `rustc_codegen_gcc` is building against GCC master and
|
||
there were many missing symbols.
|
||
|
||
Undeterred I noted that there is a WIP GCC 12.2 branch in the Retro68 repo so I
|
||
built that and tweaked `rustc_codegen_gcc` to disable the `master` cargo
|
||
feature that should in theory allow it to build against a GCC release. This did
|
||
in fact allow me to get a bit further but I ran into more issues in the step
|
||
that attempts to build `compiler-rt` and `core`. Eventually I gave up on this
|
||
route too. I was probably too far off the well tested configuration of x86,
|
||
against GCC master.
|
||
|
||
Future work here is to trying building a `powerpc-ibm-aix` libgccjit from GCC
|
||
master and see if that works.
|
||
|
||
### Wrap Up
|
||
|
||
[Bastian on Twitter](https://twitter.com/turbolent/status/1617231570573873152)
|
||
has had some success compiling Rust to Web Assembly, Web Assembly to C89, C89
|
||
to Mac OS 9 binary, which is definitely cool but I would still love to be able
|
||
to generate native PPC code directly from `rustc` somehow.
|
||
|
||
This is where I have parked this project for now. I actually only discovered
|
||
the post on building a Mac OS 9 application with Swift while writing this post.
|
||
There are perhaps some ideas in there that I could explore further.
|
||
|
||
[swift]: https://belkadan.com/blog/2020/04/Swift-on-Mac-OS-9/
|
||
[BlueSCSI]: https://github.com/erichelgeson/BlueSCSI
|
||
[Retro68]: https://github.com/autc04/Retro68
|
||
[SheepShaver]: https://sheepshaver.cebix.net/
|
||
[Dialog sample]: https://github.com/autc04/Retro68/tree/5f882506013a0a8a4335350197a1b7c91763494e/Samples/Dialog
|
||
[Pascal string]: https://en.wikipedia.org/wiki/String_(computer_science)#Length-prefixed
|
||
[libgccjit]: https://gcc.gnu.org/onlinedocs/jit/
|