Repository navigation
JNI: Swift class hierarchies as Java class hierarchies - #8
Open
piercifani wants to merge 2 commits into
Open
piercifani wants to merge 2 commits into
piercifani wants to merge 2 commits into
Conversation
piercifani
force-pushed
the
jni/07-class-inheritance
branch
from
November 3, 2026 14:41
5fb45a6 to
12d62f7
Compare
piercifani
force-pushed
the
jni/07-class-inheritance
branch
from
November 3, 2026 15:19
12d62f7 to
76dce71
Compare
piercifani
force-pushed
the
jni/07-class-inheritance
branch
from
November 3, 2026 15:56
76dce71 to
2de65e8
Compare
Every Swift class became an unrelated final Java class, so a subclass couldn't be used as its superclass, didn't expose inherited members, and an instance returned as its superclass couldn't be cast to its own class. - A class whose superclass is extracted in the same module extends the superclass's Java class. `open` classes and classes with extracted subclasses are no longer final and get a protected designated constructor; subclasses call `super(...)`, so the pointer and cleanup live in the root class. - A class with extracted subclasses gets a `$dynamicTypeMetadataAddress` thunk, and its `wrapMemoryAddressUnsafe` wraps an instance in the Java class of its dynamic type, deepest subclass first. - Inherited members need no new thunks: a class instance is a pointer to one reference, which the superclass's thunks read through the same layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ose uses it Animal/Dog for subclass-as-superclass and dynamic-class returns; and the full view-model shape: a main-actor observable open base class, a subclass with callbacks taking structs, an input enum with payloads, and async loading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
piercifani
force-pushed
the
jni/07-class-inheritance
branch
from
November 3, 2026 16:46
2de65e8 to
002d8af
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack 7/21 · base:
jni/06-java-implemented-protocols· previous: #7 · next: #10Problem
In JNI mode every Swift class becomes an unrelated
public final class … implements JNISwiftInstance. ForDogdoesn't extendAnimal. SoAnimal a = dogdoesn't compile, aDogcan't be passed where anAnimalis expected,Dogdoesn't getdescribe(), andmakeDog()returns a plainAnimalwrapper that can't be cast toDog.Changes
extends: a class whose superclass is extracted in the same module extends the superclass's Java class and no longer re-declaresimplements JNISwiftInstance. Only concrete classes are affected; generic classes, specializations and error types are unchanged.openclass, or one with extracted subclasses, is no longerfinaland gets aprotecteddesignated constructor. Final classes with no hierarchy are printed exactly as before.super(selfPointer, swiftArena). The pointer,$memoryAddress()and the cleanup live in the root class. The cleanup's destroy function already uses the virtual$typeMetadataAddress(), so each instance is destroyed with its own class's metadata.$dynamicTypeMetadataAddress(selfPointer)thunk (type(of:)on the instance). ItswrapMemoryAddressUnsafecompares the result against each subclass's$typeMetadataAddressDowncall(), deepest subclass first, and wraps in the most specific Java class. Every call site that wraps a returned instance gets this without other changes. A subclass's metadata accessor becomes package-private so its base classes can call it.Why Skip needs it
Skip Fuse view models share one
@MainActor @Observable open classbase. In a production Skip Fuse app (16 Skip modules), 189 view models subclass it, and Compose code passes them around as the base type.Kotlin in this PR
ClassHierarchyTest.ktwithClassHierarchy.swift: aDogused as anAnimal, passed where anAnimalis expected, and returned asAnimalbut received as aDog.ViewModelTest.ktwithViewModels.swift: a view model as Compose code uses it, built on every PR below this one. A@MainActor @Observable openbase class; a subclass with aCallbacksstruct of@MainActor @Sendableclosures taking structs; an input enum with payloads; and anasyncmain-actorload(), awaited by pumping the main run loop (on Android the main looper does this). macOS only.Testing
./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:testpasses at this point in the stack.JNIClassInheritanceTests(6 tests). The 5 for the change fail on the base branch; the control (a plain final class prints as before) passes on both.Shape/Squarewith Java tests for subclass-as-superclass, inherited and overridden members, an array of the base type, and aShapereturn that is aSquarein Java.Animal/Dog: subclass as base, subclass passed as base, and a base return keeping its dynamic type. The other is the full Skip view-model shape: a@MainActor @Observable openbase, a subclass with aCallbacksstruct of@MainActor @Sendableclosures, an input enum with payloads, and anasyncmain-actorload(). The last one builds on the earlier PRs in this stack.swift test --disable-experimental-prebuilts --disable-sandboxand./gradlew :Samples:SwiftJavaExtractJNISampleApp:testpass.swift format lint --stricton changed files;git diff --check.Not in this PR
🤖 Generated with Claude Code