Skip to content

JNI: wrappers of the same Swift object are equal; arena-free getCase() - #13

Open
piercifani wants to merge 2 commits into
jni/08-async-cancellationfrom
jni/09-object-identity
Open

piercifani wants to merge 2 commits into
jni/08-async-cancellationfrom
jni/09-object-identity

Conversation

@piercifani

@piercifani piercifani commented Nov 3, 2026 •

Copy link
Copy Markdown

Stack 9/21 · base: jni/08-async-cancellation · previous: #10 · next: #14

Problem

Two Java wrappers of the same Swift object weren't equal. Each returned class instance gets its own pointer box. For a class that isn't Equatable, SwiftObjects.equals compared the box addresses, so Session.shared() called twice gave two unequal objects with different hash codes. Kotlin ==, HashSet/HashMap, and Compose's change detection (which skips recomposition for equal values) all depend on this.

getCase() on an enum with payloads always needed an arena, even in allowGlobalAutomatic mode, where every other member also gets an arena-free overload.

Changes

  • SwiftObjects.equals: when both sides are class instances, load the references and compare them with === first. Wrappers of the same instance are equal whatever their static type, for example a Dog wrapper and an Animal wrapper of the same object. Different instances of an Equatable class still compare with ==; other classes compare by identity. Value types are unchanged.
  • SwiftObjects.hashCode: classes that aren't Hashable hash with ObjectIdentifier of the instance. A class that's Equatable but not Hashable hashes by its type instead, so objects that compare equal always hash the same.
  • getCase(): with allowGlobalAutomatic, enums whose cases need an arena also get getCase(), which uses the default automatic arena. In Kotlin that reads as the property route.case.

Kotlin in this PR

IdentityTest.kt with Identity.swift:

  • Session.getShared() twice gives equal objects, and a setOf of them has one element.
  • A Dog passed through a function typed Animal comes back equal, with the same hash code.
  • route.case works without an arena.

Why Skip needs it

Skip's bridge compares Kotlin peers by the Swift object they hold, and Compose state relies on that equality. A production Skip Fuse app (16 Skip modules) bridges about 1,200 classes this way.

Testing

  • Kotlin sample: ./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes at this point in the stack.
  • New JNIEnumGetCaseArenaTests: the overload exists in allowGlobalAutomatic mode and not in explicit mode.
  • JNI sample: a Square round-tripped through a Shape parameter equals the original and has the same hash code, and a different Square doesn't equal it.
  • A standalone Swift 6 package's Kotlin tests: a shared instance compares equal, and GC-driven release still works.
  • Full swift test --disable-experimental-prebuilts --disable-sandbox and ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test pass.
  • swift format lint --strict on changed files; git diff --check.

Not in this PR

  • Reusing one Java wrapper per Swift object (a weak cache). Equality is by identity now, but === on wrappers is still false.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 3, 2026 17:35
Each returned class instance gets its own pointer box, and for classes that
aren't Equatable `SwiftObjects.equals` compared the boxes, so two wrappers of
one object were never equal and hashed differently.

- `equals`: when both sides are class instances, compare the objects with `===`
  first (wrappers of one instance are equal whatever their static type); other
  instances of an Equatable class still use `==`.
- `hashCode`: non-Hashable classes hash by `ObjectIdentifier`, or by type when
  they are Equatable, so equal objects always hash the same.
- With `allowGlobalAutomatic`, enums whose cases need an arena also get an
  arena-free `getCase()`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@piercifani
piercifani force-pushed the jni/09-object-identity branch from 5706c92 to 22e6da6 Compare November 3, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant