Skip to content

Kotlin / Java bindings

The Kotlin/JVM bindings expose the Rust SDK as com.fatoora classes through JNA. Java callers use static factories and overloads with ordinary return values; native failures throw BindingException. The public API does not expose Kotlin Result or unsigned types with JVM-mangled method names.

Build and install

See the package README for local Maven installation, Android SDK setup and test commands. Packages are CI artifacts and local builds; Maven Central publication is not configured.

Artifact Runtime Native platforms in CI
com.fatoora:fatoority:0.4.2 Java 8+ Linux x86-64, Windows x86-64, macOS arm64
com.fatoora:fatoority-android:0.4.2 Android API 26+ arm64-v8a, x86_64

The desktop JAR includes JNA native resource directories. Linux CI assets require glibc 2.28+; musl/Alpine is not included. Local native builds inherit the host's libc requirements. Android libraries are separate NDK builds, packaged under jni/<abi> in the AAR. Desktop binaries cannot be used on Android.

Windows x86-64 also requires the Microsoft Visual C++ v14 x64 runtime. The native DLL imports VCRUNTIME140.dll; it is not bundled in the JAR.

Kotlin and Java

import com.fatoora.*

SigningKey.generate().use { key ->
    SigningKey.fromPem(key.toPem()).use { restored ->
        check(key.toPem() == restored.toPem())
    }
}
Address.create("SA", "الرياض", "Street", "1234", "12345", null, "", "حي العليا").use { address ->
    println(address.city())
    check(address.additionalStreet() == null)
    address.additionalNumber()?.use { check(it.value() == "") }
}
import com.fatoora.*;

try (SigningKey key = SigningKey.generate();
     SigningKey restored = SigningKey.fromPem(key.toPem())) {
    if (!key.toPem().equals(restored.toPem())) throw new AssertionError();
}
try {
    Config.create(99);
} catch (BindingException e) {
    System.err.println(e.getCode() + ": " + e.getMessage());
}

Use environment 0 for nonproduction, 1 for simulation, 2 for production. Configuration, CSR and credentials must agree on the environment. Native API names follow the bridge in camel case; Rust new becomes create.

Ownership and values

Bridge value Public JVM value
Owned opaque AutoCloseable; Kotlin use or Java try-with-resources
Optional text input Nullable String; null differs from ""
Optional text output Nullable Text; read value() and close it
Decimal Invariant decimal string
Unsigned byte / short / int Checked Int / Int / Long
Unsigned 64-bit counter BigInteger, range 0 through 2^64−1
Binary input / copied output ByteArray / Java byte[]
Native failure BindingException(code, message, detailsJson)

Close objects returned by methods as well as those you construct. Native errors are copied to managed data and freed immediately; exceptions need no cleanup. Bytes.asSlice() copies bytes under the owner lock, so the returned array survives closing its native owner. Strings reject lone UTF-16 surrogates and embedded NUL.

Calls acquire all participating owners in a fixed lock order. Concurrent calls on the same object serialize; close() waits for active calls and is idempotent. Later calls throw IllegalStateException. Operations on independent owners can run concurrently. Consumed builders/invoices remain closeable but reject further use with a native error. Phantom references provide nondeterministic fallback cleanup; freeing native memory does not guarantee secret erasure.

Import com.fatoora, not the generated implementation package com.fatoora.raw. Raw bindings do not provide these ownership and synchronization guarantees.

Blocking calls and coroutines

Blocking native HTTP calls are available to Java and Kotlin. Kotlin extensions postCsrForCcsidAsync, postCcsidForPcsidAsync, renewCsidAsync, checkInvoiceComplianceAsync, reportSimplifiedInvoiceAsync, and clearStandardInvoiceAsync dispatch them through withContext(Dispatchers.IO).

// Within a suspend function; keep owners open until the call completes.
client.reportSimplifiedInvoiceAsync(invoice, credentials, false).use { response ->
    response.ensureAccepted()
}

Cancellation cannot interrupt an in-flight native call. Its locks remain held until completion. If cancellation discards a completed owning result during the return dispatch, the coroutine wrapper closes that result. Use prepared requests with a cancellable JVM HTTP client when transport-level cancellation is needed.

prepareReportSimplifiedInvoice, for example, returns an owned InvoiceRequest: read url(), method(), headersJson() and body(), send exactly those values, then call decode(status, responseBytes) and ensureAccepted() on the returned response. Prepared requests own their bytes and decoding context independently of the original credentials/invoice. Headers contain credentials: do not log them.

Android

Use the Android artifact alone. It supplies JNA's Android dependency and shrinker rules and declares the Internet permission. It supports 64-bit ABIs with 16 KiB ELF alignment. Run network, signing and validation work off the UI thread; the coroutine HTTP extensions use the IO dispatcher.

Android native TLS uses bundled Mozilla/WebPKI roots. Android user-installed CAs and app network-security configuration do not modify that trust store. Prepared requests let apps use their own HTTP client and certificate policy.

CI builds both ABIs and runs device tests on API 26 and API 35 with 16 KiB pages. Tests cover Java calls, Arabic strings, nullable values, errors, client creation, frozen SDK hash parity and concurrent close. Desktop tests additionally exercise signing, prepared requests, result decoding and cancellation cleanup on Java 8.

License

Original SDK code is MIT OR Apache-2.0. Native packages contain ZATCA-derived rule predicates and messages under their own terms and include THIRD_PARTY_NOTICES.md and licenses/zatca/. See the third-party notices.