Skip to content

Support WebAssembly + fix Linux collection-cast crash#30

Open
0xLeif wants to merge 1 commit into
mainfrom
fix/wasm-cross-platform-2x
Open

Support WebAssembly + fix Linux collection-cast crash#30
0xLeif wants to merge 1 commit into
mainfrom
fix/wasm-cross-platform-2x

Conversation

@0xLeif

@0xLeif 0xLeif commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Two cross-platform fixes (baselined on 2.1.x, the line AppState 3.0 depends on).

WebAssembly build (AppState#149)

@Published/ObservableObject were gated with #if os(Linux) || os(Windows) / #if !os(Linux) && !os(Windows). WebAssembly is os(WASI) — neither Linux nor Windows — so wasm took the Combine path and failed to build (unknown attribute 'Published', cannot find type 'ObservableObject'). Switched both to #if canImport(Combine), which correctly excludes Linux, Windows, and wasm.

Linux collection-cast crash (AppState#151)

Dictionary.get(_:as:) did self[key] as? Item. When the store is Cache<String, Any> and Item is a collection (e.g. [SomeStruct]), casting Optional<Any> → [T] takes the runtime _arrayForceCast path, which aborts (swift_dynamicCastFailure) on Linux/WASI. Boxing through Any first — (self[key] as Any) as? Item — normalises the cast and avoids the abort.

Tests

Added array round-trip regression tests through Cache<String, Any>. 178 tests pass on macOS.

Closes the Cache side of AppState#149 and AppState#151.

- Guard @published / ObservableObject with #if canImport(Combine) instead of
  #if !os(Linux) && !os(Windows). The latter includes os(WASI), so wasm took the
  Combine path and failed to build (unknown attribute 'Published'). canImport(Combine)
  correctly excludes Linux, Windows, AND wasm. (AppState#149)
- Dictionary.get(_:as:): box through Any before the conditional cast
  ((self[key] as Any) as? Item). Casting Optional<Any> -> [ConcreteType] takes the
  runtime _arrayForceCast path, which aborts (swift_dynamicCastFailure) on Linux/WASI
  for collection-typed values. (AppState#151)
- Add array round-trip regression tests through Cache<String, Any>.

178 tests pass on macOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves platform compatibility (particularly for Linux and WebAssembly/WASI) by replacing OS-specific checks with #if canImport(Combine) and addressing a Swift runtime casting bug when retrieving collection-typed values from an Any cache. Review feedback suggests a more idiomatic way to resolve the casting bug by unwrapping the optional value first rather than boxing it to Any, and recommends explicitly importing Combine when extending Cache with ObservableObject to ensure reliable compilation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +18 to 26
// Route through an explicit `Any` boxing step before the conditional cast. This avoids
// `swift_dynamicCastFailure` / `_arrayForceCast` aborts on Linux and WASI when casting an
// `Optional<Any>` that wraps a collection type (e.g. `[SomeStruct]`): without the
// intermediate cast, the runtime takes the `Optional<Any>` → `[T]` forced-array-cast path,
// which is not implemented the same way off Darwin. Boxing to `Any` first normalises the
// dynamic type to the concrete `Item` path.
guard let value = (self[key] as Any) as? Item else {
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Instead of boxing the optional value into Any via (self[key] as Any) as? Item, we can simply unwrap the optional value first using guard let rawValue = self[key]. This completely avoids the buggy Optional<Any> to [T] cast path on Linux/WASI, is much more idiomatic, and avoids potential edge cases where nil is boxed into Any (which could behave unexpectedly if Item is Any or an optional type).

Suggested change
// Route through an explicit `Any` boxing step before the conditional cast. This avoids
// `swift_dynamicCastFailure` / `_arrayForceCast` aborts on Linux and WASI when casting an
// `Optional<Any>` that wraps a collection type (e.g. `[SomeStruct]`): without the
// intermediate cast, the runtime takes the `Optional<Any>` → `[T]` forced-array-cast path,
// which is not implemented the same way off Darwin. Boxing to `Any` first normalises the
// dynamic type to the concrete `Item` path.
guard let value = (self[key] as Any) as? Item else {
return nil
}
// Unwrapping the optional value first avoids a Swift runtime bug on Linux and WASI
// where casting `Optional<Any>` directly to a collection type (e.g. `[T]`) aborts
// with `swift_dynamicCastFailure`.
guard let rawValue = self[key], let value = rawValue as? Item else {
return nil
}

Comment on lines +166 to 168
#if canImport(Combine)
extension Cache: ObservableObject { }
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure that @Published and ObservableObject compile correctly across all platforms and build configurations (especially when this file is compiled incrementally or in isolation), we should explicitly import Combine when it is available.

Suggested change
#if canImport(Combine)
extension Cache: ObservableObject { }
#endif
#if canImport(Combine)
import Combine
extension Cache: ObservableObject { }
#endif

0xLeif added a commit to 0xLeif/AppState that referenced this pull request Jun 11, 2026
#149 (WASM): switch Combine/OSLog/SwiftUI-gated #if !os(Linux) && !os(Windows)
guards to their canImport(...) checks. os(WASI) is neither Linux nor Windows, so the
old guards pulled Combine/SwiftUI into the wasm build. Keychain/iCloud stay Apple-only.

#150 (observation off Apple): delivery was bridged through Combine's
consume(object: cache) (Apple-only), so withObservationTracking never fired on
Linux/Windows/wasm. Now every mutation path (State/StoredState/FileState/SyncState/
SecureState/DependencySlice) calls notifyChange() directly on all platforms, and the
Combine cache bridge is removed so Apple still fires exactly once.

163 tests pass on macOS. Observation tests stay gated off Linux/Windows: swift-corelibs
-xctest cannot discover synchronous @mainactor test methods there (it force-casts test
thunks to () -> () and aborts) — a test-harness limitation, not a runtime one. The #150
delivery code itself is cross-platform. Full wasm build + Linux collection-typed state
also need the Cache fix (0xLeif/Cache#30).

Addresses #149, #150.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant