Skip to content

Commit 3b460cd

Browse files
proggeramlugRalph Küpper
andauthored
feat(watchos): arm64_32 device support (Apple Watch Series 4-8 / SE) (#5060)
* feat(watchos): arm64_32 device support (Series 4-8 / SE) Real pre-S9 Apple Watches use the arm64_32 architecture (64-bit ISA, 32-bit pointers). Perry never produced a working binary for them, and an old note claimed it was architecturally impossible (NaN-boxing needs 64-bit usize). That's overstated: a 32-bit pointer fits trivially in the 48-bit NaN payload — only a few heap-range guard heuristics and literal constants assumed a 64-bit address space. Runtime (32-bit literal fixes; also benefit wasm32): - array/from_concat: (1usize << 53) saturates via try_from on 32-bit - box.rs / formatting.rs / value/dynamic_object.rs: 2^47/2^48 pointer- guard upper bounds compared in u64 so the literal isn't out-of-range for 32-bit usize (no-op on 32-bit — no addresses reach that high) The heap-min *value* guards already pick the low (0x1000) floor on target_os=watchos, so no value changes were needed there. Compile driver (opt-in via PERRY_WATCHOS_ARM64_32, so the default watchos=arm64/S9+ path is untouched): - codegen emits the C entry via PERRY_ENTRY_SYMBOL (e.g. _perry_user_main) instead of renaming _main with rust-objcopy afterwards — objcopy's MachOWriter segfaults on arm64_32 Mach-O. The link-time objcopy rename is skipped on arm64_32 accordingly. - helpers.rs / app_metadata.rs / link/mod.rs / platform_cmd.rs / bundle_apple.rs resolve the arm64_32-apple-watchos triple (codegen, auto-optimize runtime rebuild, native-lib build, swiftc, link) and a low MinimumOSVersion (PERRY_WATCHOS_MIN, default 11.0). Validated end to end: built a real game (Bloom Jump) — perry-runtime + perry-ui-watchos + engine native lib all compile for arm64_32-apple- watchos; the game links to a Mach-O 'architecture: arm64_32, platform WATCHOS, minos 11.0' with __perry_user_main and _main both defined and no undefined refs. (Testable only on real pre-S9 hardware — the simulator is arm64.) Co-authored from a parked WIP exploring this target. * style: rustfmt the arm64_32 entry-symbol changes --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent 3dc1c94 commit 3b460cd

10 files changed

Lines changed: 66 additions & 13 deletions

File tree

crates/perry-codegen/src/codegen/entry.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,15 @@ pub(super) fn compile_module_entry(
132132
let main = if is_dylib {
133133
llmod.define_function("perry_module_init", VOID, vec![])
134134
} else {
135-
llmod.define_function("main", I32, vec![])
135+
// Allow the host build to override the C entry symbol. On arm64_32
136+
// watchOS we can't rename `_main → __perry_user_main` after the
137+
// fact (rust-objcopy's MachOWriter crashes on arm64_32 objects), so
138+
// we emit the final symbol directly. Pass e.g. `_perry_user_main`
139+
// (the leading underscore yields Mach-O `__perry_user_main`, which
140+
// the Swift `@main` shell references via @_silgen_name).
141+
let entry_name =
142+
std::env::var("PERRY_ENTRY_SYMBOL").unwrap_or_else(|_| "main".to_string());
143+
llmod.define_function(&entry_name, I32, vec![])
136144
};
137145
main.add_pre_return_void_call("js_typed_feedback_maybe_dump_trace");
138146
let _ = main.create_block("entry");

crates/perry-codegen/src/codegen/helpers.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,12 @@ pub fn resolve_target_triple(name: &str) -> Option<String> {
397397
"ios-simulator" => Some("arm64-apple-ios17.0-simulator".to_string()),
398398
"visionos" => Some("arm64-apple-xros1.0".to_string()),
399399
"visionos-simulator" => Some("arm64-apple-xros1.0-simulator".to_string()),
400+
// arm64_32 (Series 4-8 / SE) when opted in via PERRY_WATCHOS_ARM64_32;
401+
// otherwise arm64 (S9+). Sets the arch of the emitted TS object files,
402+
// which must match the runtime/native-lib/link triples.
403+
"watchos" if std::env::var("PERRY_WATCHOS_ARM64_32").is_ok() => {
404+
Some("arm64_32-apple-watchos".to_string())
405+
}
400406
"watchos" => Some("aarch64-apple-watchos".to_string()),
401407
"watchos-simulator" => Some("arm64-apple-watchos10.0-simulator".to_string()),
402408
"tvos" => Some("aarch64-apple-tvos".to_string()),

crates/perry-runtime/src/array/from_concat.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,9 @@ fn array_like_length(items: f64) -> usize {
413413
let n = n.floor();
414414
let max = (1u64 << 53) as f64 - 1.0;
415415
if n > max {
416-
return (1usize << 53) - 1;
416+
// 2^53 - 1 (JS max safe integer). usize can't represent this on 32-bit
417+
// targets (arm64_32 watchOS, wasm32), so saturate to usize::MAX there.
418+
return usize::try_from((1u64 << 53) - 1).unwrap_or(usize::MAX);
417419
}
418420
n as usize
419421
}

crates/perry-runtime/src/box.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub fn scan_box_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) {
8787
// address (alloc gives 8-aligned pointers in user space)
8888
// matches `is_plausible_box_ptr` to keep this a no-op for
8989
// any pathological entry.
90-
if addr >= 0x1000 && addr < 0x0001_0000_0000_0000 && addr % 8 == 0 {
90+
if addr >= 0x1000 && (addr as u64) < 0x0001_0000_0000_0000 && addr % 8 == 0 {
9191
unsafe {
9292
visitor.visit_nanbox_f64_raw_slot(&raw mut (*ptr).value);
9393
}
@@ -200,7 +200,7 @@ fn is_plausible_box_ptr(ptr: *mut Box) -> bool {
200200
if addr < 0x1000 {
201201
return false;
202202
}
203-
if addr >= 0x0001_0000_0000_0000 {
203+
if (addr as u64) >= 0x0001_0000_0000_0000 {
204204
return false;
205205
}
206206
if addr % std::mem::align_of::<Box>() != 0 {

crates/perry-runtime/src/builtins/formatting.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1694,7 +1694,10 @@ fn looks_like_raw_heap_pointer(value: f64) -> bool {
16941694
return false;
16951695
}
16961696
let addr = bits as usize;
1697-
(0x1000..0x8000_0000_0000usize).contains(&addr) && addr >= crate::gc::GC_HEADER_SIZE + 0x1000
1697+
// Compare in u64 so the 2^47 upper bound stays in range on 32-bit targets
1698+
// (arm64_32 watchOS, wasm32), where it's a no-op — no addresses that high.
1699+
(0x1000..0x8000_0000_0000u64).contains(&(addr as u64))
1700+
&& addr >= crate::gc::GC_HEADER_SIZE + 0x1000
16981701
}
16991702

17001703
fn formatted_deep_equal(left: f64, right: f64, skip_prototype: bool) -> bool {

crates/perry-runtime/src/value/dynamic_object.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub extern "C" fn js_value_length_f64(value: f64) -> f64 {
8787
target_os = "visionos",
8888
)))]
8989
let heap_min: usize = 0x200_0000_0000;
90-
if handle < heap_min || handle >= 0x8000_0000_0000 {
90+
if handle < heap_min || (handle as u64) >= 0x8000_0000_0000 {
9191
return 0.0;
9292
}
9393
if let Some(value) = unsafe {

crates/perry/src/commands/compile/app_metadata.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ pub(super) fn rust_target_triple(target: Option<&str>) -> Option<&'static str> {
158158
Some("visionos-simulator") => Some("aarch64-apple-visionos-sim"),
159159
Some("visionos") => Some("aarch64-apple-visionos"),
160160
Some("watchos-simulator") => Some("aarch64-apple-watchos-sim"),
161+
// arm64_32 watchOS (Series 4-8 / SE) when opted in; otherwise arm64
162+
// (S9+). Governs the rust target used for the auto-optimize runtime
163+
// rebuild and for building/resolving native libraries, so all three
164+
// must agree with the link triple in platform_cmd.rs / link/mod.rs.
165+
Some("watchos") if std::env::var("PERRY_WATCHOS_ARM64_32").is_ok() => {
166+
Some("arm64_32-apple-watchos")
167+
}
161168
Some("watchos") => Some("aarch64-apple-watchos"),
162169
Some("tvos-simulator") => Some("aarch64-apple-tvos-sim"),
163170
Some("tvos") => Some("aarch64-apple-tvos"),

crates/perry/src/commands/compile/bundle_apple.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,17 @@ pub(super) fn bundle_for_watchos(
173173
// #4849: read version/build_number from perry.toml (was hardcoded "1.0").
174174
let (app_version, app_build_number) = read_apple_app_version(input);
175175

176-
// Device builds are arm64-only, which requires watchOS 26 (S9+); the
177-
// simulator target keeps the lower floor.
176+
// Device builds default to arm64-only, which requires watchOS 26 (S9+).
177+
// arm64_32 device builds (PERRY_WATCHOS_ARM64_32) reach pre-S9 watches and
178+
// use a low floor (overridable via PERRY_WATCHOS_MIN); the simulator target
179+
// keeps the lower floor too.
180+
let arm64_32 = target == Some("watchos") && std::env::var("PERRY_WATCHOS_ARM64_32").is_ok();
181+
let min_os_owned;
178182
let min_os = if target == Some("watchos-simulator") {
179183
"10.0"
184+
} else if arm64_32 {
185+
min_os_owned = std::env::var("PERRY_WATCHOS_MIN").unwrap_or_else(|_| "11.0".to_string());
186+
min_os_owned.as_str()
180187
} else {
181188
"26.0"
182189
};

crates/perry/src/commands/compile/link/mod.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,11 +1762,20 @@ pub(super) fn build_and_run_link(
17621762
} else {
17631763
"watchos"
17641764
};
1765+
// arm64_32 watchOS (Series 4-8 / SE): opt-in, matches the app
1766+
// binary's triple in platform_cmd.rs so the native @main lib
1767+
// links against the same arch.
1768+
let swift_arm64_32 =
1769+
target == Some("watchos") && std::env::var("PERRY_WATCHOS_ARM64_32").is_ok();
1770+
let swift_watchos_min =
1771+
std::env::var("PERRY_WATCHOS_MIN").unwrap_or_else(|_| "11.0".to_string());
1772+
let swift_triple_owned;
17651773
let swift_triple = if target == Some("watchos-simulator") {
17661774
"arm64-apple-watchos10.0-simulator"
1775+
} else if swift_arm64_32 {
1776+
swift_triple_owned = format!("arm64_32-apple-watchos{}", swift_watchos_min);
1777+
swift_triple_owned.as_str()
17671778
} else {
1768-
// Device builds are arm64-only (S9+ / watchOS 26): Perry's
1769-
// NaN-boxed values need 64-bit pointers, which arm64_32 lacks.
17701779
"arm64-apple-watchos26.0"
17711780
};
17721781
let swift_sysroot = String::from_utf8(

crates/perry/src/commands/compile/link/platform_cmd.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,19 @@ pub fn select_linker_command(
5353
)?
5454
.trim()
5555
.to_string();
56+
// arm64_32 watchOS (Series 4-8 / SE): opt-in via PERRY_WATCHOS_ARM64_32.
57+
// Lets the device target reach pre-S9 watches; deployment min defaults
58+
// low (these watches run watchOS 9-11) but is overridable.
59+
let arm64_32 = target == Some("watchos") && std::env::var("PERRY_WATCHOS_ARM64_32").is_ok();
60+
let watchos_min = std::env::var("PERRY_WATCHOS_MIN").unwrap_or_else(|_| "11.0".to_string());
61+
let triple_owned;
5662
let triple = if target == Some("watchos-simulator") {
5763
"arm64-apple-watchos10.0-simulator"
64+
} else if arm64_32 {
65+
triple_owned = format!("arm64_32-apple-watchos{}", watchos_min);
66+
triple_owned.as_str()
5867
} else {
59-
// Device builds are arm64-only (S9+ / watchOS 26): Perry's
60-
// NaN-boxed values need 64-bit pointers, which arm64_32 lacks.
68+
// Device builds default to arm64-only (S9+ / watchOS 26).
6169
"arm64-apple-watchos26.0"
6270
};
6371

@@ -108,7 +116,10 @@ pub fn select_linker_command(
108116
.unwrap_or(false)
109117
})
110118
});
111-
if let Some(entry_obj) = entry_obj {
119+
// arm64_32: rust-objcopy crashes on these Mach-O objects, so the entry
120+
// symbol was emitted directly by codegen (PERRY_ENTRY_SYMBOL) instead of
121+
// renamed here. Skip the objcopy pass entirely.
122+
if let Some(entry_obj) = entry_obj.filter(|_| !arm64_32) {
112123
let objcopy = std::env::var("HOME").ok()
113124
.map(|h| PathBuf::from(h).join(".rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/bin/rust-objcopy"))
114125
.filter(|p| p.exists())

0 commit comments

Comments
 (0)