Skip to content

Commit 559bd72

Browse files
authored
Nullness annotations (#62)
* Stab at JSpecify nullness annotation option * Make the nullable checks a bit more robust * Use JLS correct fully qualified nullables and add tests to cover that strangeness. Not all compilers seem to care, but it's the most technically correct generation. * Prep for release
1 parent ccb897f commit 559bd72

8 files changed

Lines changed: 726 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.4.2
2+
3+
- Added `nullness_annotations` config option to emit JSpecify `@NullMarked` and
4+
`@Nullable` annotations in generated code. When enabled, Rust `Option<T>` maps to
5+
`@Nullable T` and all other types are non-null by default. Requires
6+
`org.jspecify:jspecify` on the compile classpath.
7+
18
## 0.4.1
29

310
- fix resolving callback trait implementations declared in submodules of the current crate. Previously, traits like `my_crate::metrics::MetricsRecorder` failed with "no interface with module_path" during code generation.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "uniffi-bindgen-java"
3-
version = "0.4.1"
3+
version = "0.4.2"
44
authors = ["IronCore Labs <info@ironcorelabs.com>"]
55
readme = "README.md"
66
license = "MPL-2.0"

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ The generated Java can be configured using a `uniffi.toml` configuration file.
133133
| `custom_types` | | A map which controls how custom types are exposed to Java. See the [custom types section of the UniFFI manual](https://mozilla.github.io/uniffi-rs/latest/udl/custom_types.html#custom-types-in-the-bindings-code) |
134134
| `external_packages` | | A map of packages to be used for the specified external crates. The key is the Rust crate name, the value is the Java package which will be used referring to types in that crate. See the [external types section of the manual](https://mozilla.github.io/uniffi-rs/latest/udl/ext_types_external.html#kotlin) |
135135
| `rename` | | A map to rename types, functions, methods, and their members in the generated Java bindings. See the [renaming section](https://mozilla.github.io/uniffi-rs/latest/renaming.html). |
136+
| `nullness_annotations` | `false` | Generate [JSpecify](https://jspecify.dev/) nullness annotations. Rust `Option<T>` maps to `@Nullable T`; all other types are non-null by default via `@NullMarked`. Requires `org.jspecify:jspecify` on the compile classpath. See [Nullness Annotations](#nullness-annotations). |
136137
| `android` | `false` | Generate [PanamaPort](https://github.com/vova7878/PanamaPort)-compatible code for Android. Replaces `java.lang.foreign.*` with `com.v7878.foreign.*` and `java.lang.invoke.VarHandle` with `com.v7878.invoke.VarHandle`. Requires PanamaPort `io.github.vova7878.panama:Core` as a runtime dependency and Android API 26+. |
137138
| `omit_checksums` | `false` | Whether to omit checking the library checksums as the library is initialized. Changing this will shoot yourself in the foot if you mixup your build pipeline in any way, but might speed up initialization. |
138139

@@ -174,6 +175,63 @@ Where `<namespace>` is the UniFFI namespace of your component (e.g., `arithmetic
174175

175176
You can also pass a plain library name as the override, in which case it behaves like `System.loadLibrary()` and still requires the library to be on `java.library.path`.
176177

178+
## Nullness Annotations
179+
180+
Generated bindings can include [JSpecify](https://jspecify.dev/) nullness annotations so that
181+
Kotlin consumers get proper nullable/non-null types and Java consumers get IDE and static
182+
analysis support.
183+
184+
Enable in `uniffi.toml`:
185+
186+
```toml
187+
[bindings.java]
188+
nullness_annotations = true
189+
```
190+
191+
When enabled:
192+
- A `package-info.java` is generated with `@NullMarked`, making all types non-null by default
193+
- Rust `Option<T>` types are annotated with `@Nullable`, including inside generic type
194+
arguments (e.g., `Map<String, @Nullable Integer>` for `HashMap<String, Option<i32>>`)
195+
- All non-optional types (primitives, strings, records, objects, enums) are non-null
196+
197+
### Build Setup
198+
199+
JSpecify must be on the compile classpath when compiling the generated Java source.
200+
201+
**Gradle:**
202+
```kotlin
203+
// Use `api` if publishing a library so Kotlin/Java consumers benefit automatically.
204+
// Use `compileOnly` if the bindings are only used within this project.
205+
dependencies {
206+
api("org.jspecify:jspecify:1.0.0")
207+
}
208+
```
209+
210+
**Maven:**
211+
```xml
212+
<!-- Use default scope if publishing a library. Use <scope>provided</scope> for internal use. -->
213+
<dependency>
214+
<groupId>org.jspecify</groupId>
215+
<artifactId>jspecify</artifactId>
216+
<version>1.0.0</version>
217+
</dependency>
218+
```
219+
220+
There is no runtime dependency — the JVM ignores annotation classes that are not present at
221+
runtime.
222+
223+
### Kotlin Interop
224+
225+
Without nullness annotations, Kotlin sees all Java types from the generated bindings as
226+
**platform types** (`String!`), which bypass null-safety checks. With annotations enabled,
227+
Kotlin correctly maps:
228+
229+
- Non-optional types → non-null (`String`, `MyRecord`)
230+
- `Option<T>` types → nullable (`String?`, `MyRecord?`)
231+
232+
This requires JSpecify to be on Kotlin's compile classpath (automatic if declared with `api`
233+
scope).
234+
177235
## Notes
178236

179237
- failures in CompletableFutures will cause them to `completeExceptionally`. The error that caused the failure can be checked with `e.getCause()`. When implementing an async Rust trait in Java, you'll need to `completeExceptionally` instead of throwing. See `TestFixtureFutures.java` for an example trait implementation with errors.

src/gen_java/compounds.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@ impl OptionalCodeType {
2222

2323
impl CodeType for OptionalCodeType {
2424
fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String {
25-
super::JavaCodeOracle
25+
let inner = super::JavaCodeOracle
2626
.find(self.inner())
27-
.type_label(ci, config)
28-
.to_string()
27+
.type_label(ci, config);
28+
if config.nullness_annotations() {
29+
super::nullable_type_label(&inner)
30+
} else {
31+
inner
32+
}
2933
}
3034

3135
fn canonical_name(&self) -> String {

0 commit comments

Comments
 (0)