Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.4.2

- Added `nullness_annotations` config option to emit JSpecify `@NullMarked` and
`@Nullable` annotations in generated code. When enabled, Rust `Option<T>` maps to
`@Nullable T` and all other types are non-null by default. Requires
`org.jspecify:jspecify` on the compile classpath.

## 0.4.1

- 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.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "uniffi-bindgen-java"
version = "0.4.1"
version = "0.4.2"
authors = ["IronCore Labs <info@ironcorelabs.com>"]
readme = "README.md"
license = "MPL-2.0"
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ The generated Java can be configured using a `uniffi.toml` configuration file.
| `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) |
| `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) |
| `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). |
| `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). |
| `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+. |
| `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. |

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

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`.

## Nullness Annotations

Generated bindings can include [JSpecify](https://jspecify.dev/) nullness annotations so that
Kotlin consumers get proper nullable/non-null types and Java consumers get IDE and static
analysis support.

Enable in `uniffi.toml`:

```toml
[bindings.java]
nullness_annotations = true
```

When enabled:
- A `package-info.java` is generated with `@NullMarked`, making all types non-null by default
- Rust `Option<T>` types are annotated with `@Nullable`, including inside generic type
arguments (e.g., `Map<String, @Nullable Integer>` for `HashMap<String, Option<i32>>`)
- All non-optional types (primitives, strings, records, objects, enums) are non-null

### Build Setup

JSpecify must be on the compile classpath when compiling the generated Java source.

**Gradle:**
```kotlin
// Use `api` if publishing a library so Kotlin/Java consumers benefit automatically.
// Use `compileOnly` if the bindings are only used within this project.
dependencies {
api("org.jspecify:jspecify:1.0.0")
}
```

**Maven:**
```xml
<!-- Use default scope if publishing a library. Use <scope>provided</scope> for internal use. -->
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
```

There is no runtime dependency — the JVM ignores annotation classes that are not present at
runtime.

### Kotlin Interop

Without nullness annotations, Kotlin sees all Java types from the generated bindings as
**platform types** (`String!`), which bypass null-safety checks. With annotations enabled,
Kotlin correctly maps:

- Non-optional types → non-null (`String`, `MyRecord`)
- `Option<T>` types → nullable (`String?`, `MyRecord?`)

This requires JSpecify to be on Kotlin's compile classpath (automatic if declared with `api`
scope).

## Notes

- 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.
Expand Down
10 changes: 7 additions & 3 deletions src/gen_java/compounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ impl OptionalCodeType {

impl CodeType for OptionalCodeType {
fn type_label(&self, ci: &ComponentInterface, config: &Config) -> String {
super::JavaCodeOracle
let inner = super::JavaCodeOracle
.find(self.inner())
.type_label(ci, config)
.to_string()
.type_label(ci, config);
if config.nullness_annotations() {
super::nullable_type_label(&inner)
} else {
inner
}
}

fn canonical_name(&self) -> String {
Expand Down
Loading
Loading