Skip to content

JNI: nullableAnnotation and nonNullAnnotation for Kotlin types - #24

Open
piercifani wants to merge 2 commits into
jni/19-enum-swift-case-namesfrom
jni/20-nullability-annotations
Open

piercifani wants to merge 2 commits into
jni/19-enum-swift-case-namesfrom
jni/20-nullability-annotations

Conversation

@piercifani

@piercifani piercifani commented Nov 3, 2026 •

Copy link
Copy Markdown

Stack 20/21 · base: jni/19-enum-swift-case-names · previous: #23 · next: #25 (jni/21-error-values)

Problem

Kotlin sees the parameters and results of jextract's Java methods as platform types, such as String!. Even with optionalsAsNullable (#19), Quotes.last("a") was a Quote!, not a Quote?. Kotlin neither required the null check for an optional nor knew a non-optional was never null.

Changes

Two new options name the Java annotation for each case:

  • nullableAnnotation marks optionals when optionalsAsNullable is on.
  • nonNullAnnotation marks every other reference type: strings, jextracted types, Optional, futures and arrays.

Examples are androidx.annotation.Nullable and NonNull, or JetBrains' Nullable and NotNull.

@Nullable
public static Quote last(@NotNull java.lang.String symbol) {
  • What gets annotated: a declaration's own parameters and result, after every other conversion. So an async method's future and a stream's SwiftAsyncStream are non-null. Primitives aren't annotated.
  • Imports: each configured annotation gets a single-type import, which takes precedence over SwiftKitCore's own org.swift.swiftkit.core.annotations.*.
  • No dependency: the app supplies the annotation. Android apps already have androidx.annotation, and Kotlin's stdlib brings JetBrains' annotations.
  • Not supported: the annotation must be applicable to methods and parameters. Type-use only annotations, such as JSpecify's, can't go in front of the qualified type names jextract prints (java.lang.String), so they aren't supported.

Kotlin in this PR

The Kotlin sample turns both options on with JetBrains' annotations, so every Kotlin test in it now compiles against Quote? and String rather than platform types. NullablesTest.kt:

val quote = Quotes.last("a")                  // Quote?
val price: Double = quote?.price ?: 0.0
val text: String = Quotes.describe(null, 3L)  // String

Why Skip needs it

Skip's Kotlin has real nullable and non-null types. With annotations, Kotlin code calling jextracted APIs gets the same checks it gets calling Skip-generated Kotlin.

Testing

  • Kotlin sample: ./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes (47 tests), all compiled against the annotated API.
  • New JNINullabilityAnnotationsTests (3 tests):
    • parameters, results and properties, including an async result and a Bool? result;
    • Optionals annotated non-null without optionalsAsNullable;
    • nothing annotated by default.
  • Full swift test --disable-experimental-prebuilts --disable-sandbox (690 tests), ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test (302 tests) and ./gradlew :SwiftKitCore:test pass.
  • swift run generate-config-docs --check; swift format format leaves changed files unchanged; git diff --check.

Not in this PR

  • JSpecify and other type-use annotations.
  • Closure interfaces and collection elements, which aren't annotated.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 3, 2026 21:27
Kotlin sees the parameters and results of generated Java methods as
platform types (`String!`): it neither requires null checks nor infers
nullable types. Two new options name a Java annotation for each case,
such as `androidx.annotation.Nullable`/`NonNull` or JetBrains'
`Nullable`/`NotNull`. jextract adds them to a declaration's own
parameters and results after every other conversion: `nullableAnnotation`
on optionals with `optionalsAsNullable`, `nonNullAnnotation` on every
other reference type (strings, jextracted types, `Optional`, futures,
arrays). Primitives aren't annotated. The annotations are imported by
name, ahead of SwiftKitCore's own `annotations.*`.

swift-java takes no dependency: the app supplies the annotation, and
must pick one applicable to methods and parameters. Type-use only
annotations, such as JSpecify's, don't fit before the qualified types
jextract prints, so they aren't supported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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