feat(sdk): add Swift and Kotlin mobile client foundations with push token proto
Swift SDK: Swift Package wrapping libquicproquo_ffi with QpqClient class (connect, login, send, receive, disconnect) for iOS 15+ / macOS 13+. Kotlin SDK: JNI bridge to libquicproquo_ffi with QpqClient class for Android (aarch64, armv7) and JVM, Gradle build configuration. Adds RegisterPushToken RPC (method ID 710) to device.proto for APNs/FCM/WebPush device push token registration.
This commit is contained in:
111
sdks/kotlin/README.md
Normal file
111
sdks/kotlin/README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# QuicProQuo Kotlin SDK
|
||||
|
||||
Kotlin/JVM wrapper over `libquicproquo_ffi` via JNI for Android and JVM platforms.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kotlin 1.9+ / JDK 17+
|
||||
- `libquicproquo_ffi` built for the target architecture
|
||||
- JNI bridge compiled (`jni/dev_quicproquo_NativeBridge.c`)
|
||||
|
||||
## Building the Native Library
|
||||
|
||||
```sh
|
||||
# Linux (JVM)
|
||||
cargo build --release -p quicproquo-ffi
|
||||
|
||||
# Android (aarch64)
|
||||
cargo build --release -p quicproquo-ffi --target aarch64-linux-android
|
||||
|
||||
# Android (armv7)
|
||||
cargo build --release -p quicproquo-ffi --target armv7-linux-androideabi
|
||||
```
|
||||
|
||||
### Compiling the JNI Bridge
|
||||
|
||||
```sh
|
||||
cd jni
|
||||
gcc -shared -fPIC -o libquicproquo_jni.so \
|
||||
-I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" \
|
||||
dev_quicproquo_NativeBridge.c \
|
||||
-L ../../../../target/release -lquicproquo_ffi
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Gradle
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation(files("libs/quicproquo-0.1.0.jar"))
|
||||
}
|
||||
```
|
||||
|
||||
Or include as a local project module.
|
||||
|
||||
## Usage
|
||||
|
||||
```kotlin
|
||||
import dev.quicproquo.QpqClient
|
||||
|
||||
val client = QpqClient("127.0.0.1:5001", caCertPath = "ca.pem")
|
||||
|
||||
client.login("alice", "secret")
|
||||
client.send("bob", "hello".toByteArray())
|
||||
|
||||
val messages = client.receive(timeoutMs = 5000)
|
||||
messages.forEach { println("Received: $it") }
|
||||
|
||||
client.close()
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `QpqClient(server, caCertPath, serverName)` | Connect to server |
|
||||
| `client.login(username, password)` | OPAQUE authentication |
|
||||
| `client.send(recipient, message)` | Send message by username |
|
||||
| `client.receive(timeoutMs)` | Receive pending messages |
|
||||
| `client.close()` / `client.disconnect()` | Disconnect |
|
||||
| `client.isConnected` | Connection status |
|
||||
|
||||
## Error Handling
|
||||
|
||||
```kotlin
|
||||
try {
|
||||
client.login("alice", "wrong")
|
||||
} catch (e: QpqAuthException) {
|
||||
println("Auth failed: ${e.message}")
|
||||
} catch (e: QpqTimeoutException) {
|
||||
println("Timeout: ${e.message}")
|
||||
} catch (e: QpqException) {
|
||||
println("Error: ${e.message}")
|
||||
}
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
- `src/main/kotlin/dev/quicproquo/QpqClient.kt` -- High-level client
|
||||
- `src/main/kotlin/dev/quicproquo/NativeBridge.kt` -- JNI declarations
|
||||
- `src/main/kotlin/dev/quicproquo/QpqError.kt` -- Exception types
|
||||
- `jni/dev_quicproquo_NativeBridge.c` -- JNI C bridge to FFI
|
||||
|
||||
## Android Integration
|
||||
|
||||
1. Add the native libraries to `src/main/jniLibs/<abi>/`:
|
||||
- `arm64-v8a/libquicproquo_ffi.so`
|
||||
- `armeabi-v7a/libquicproquo_ffi.so`
|
||||
2. Add the JNI bridge library alongside
|
||||
3. The `NativeBridge` class loads the library via `System.loadLibrary`
|
||||
|
||||
## Cross-Compilation
|
||||
|
||||
```sh
|
||||
# Install Android NDK targets
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi
|
||||
|
||||
# Build with the NDK linker
|
||||
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang \
|
||||
cargo build --release -p quicproquo-ffi --target aarch64-linux-android
|
||||
```
|
||||
23
sdks/kotlin/build.gradle.kts
Normal file
23
sdks/kotlin/build.gradle.kts
Normal file
@@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.9.22"
|
||||
}
|
||||
|
||||
group = "dev.quicproquo"
|
||||
version = "0.1.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
testImplementation(kotlin("test"))
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
117
sdks/kotlin/jni/dev_quicproquo_NativeBridge.c
Normal file
117
sdks/kotlin/jni/dev_quicproquo_NativeBridge.c
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* JNI bridge between Kotlin/Java and libquicproquo_ffi.
|
||||
*
|
||||
* Compile with:
|
||||
* gcc -shared -fPIC -o libquicproquo_jni.so \
|
||||
* -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" \
|
||||
* dev_quicproquo_NativeBridge.c \
|
||||
* -L ../../../../target/release -lquicproquo_ffi
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Forward declarations for libquicproquo_ffi functions. */
|
||||
typedef struct QpqHandle QpqHandle;
|
||||
|
||||
extern QpqHandle* qpq_connect(const char* server, const char* ca_cert, const char* server_name);
|
||||
extern int qpq_login(QpqHandle* handle, const char* username, const char* password);
|
||||
extern int qpq_send(QpqHandle* handle, const char* recipient,
|
||||
const uint8_t* message, size_t message_len);
|
||||
extern int qpq_receive(QpqHandle* handle, uint32_t timeout_ms, char** out_json);
|
||||
extern void qpq_disconnect(QpqHandle* handle);
|
||||
extern const char* qpq_last_error(const QpqHandle* handle);
|
||||
extern void qpq_free_string(char* ptr);
|
||||
|
||||
/* --- JNI exports --- */
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_dev_quicproquo_NativeBridge_nativeConnect(
|
||||
JNIEnv* env, jclass cls,
|
||||
jstring server, jstring caCert, jstring serverName)
|
||||
{
|
||||
const char* s = (*env)->GetStringUTFChars(env, server, NULL);
|
||||
const char* c = (*env)->GetStringUTFChars(env, caCert, NULL);
|
||||
const char* n = (*env)->GetStringUTFChars(env, serverName, NULL);
|
||||
|
||||
QpqHandle* h = qpq_connect(s, c, n);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, server, s);
|
||||
(*env)->ReleaseStringUTFChars(env, caCert, c);
|
||||
(*env)->ReleaseStringUTFChars(env, serverName, n);
|
||||
|
||||
return (jlong)(uintptr_t)h;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_dev_quicproquo_NativeBridge_nativeLogin(
|
||||
JNIEnv* env, jclass cls,
|
||||
jlong handle, jstring username, jstring password)
|
||||
{
|
||||
QpqHandle* h = (QpqHandle*)(uintptr_t)handle;
|
||||
const char* u = (*env)->GetStringUTFChars(env, username, NULL);
|
||||
const char* p = (*env)->GetStringUTFChars(env, password, NULL);
|
||||
|
||||
int code = qpq_login(h, u, p);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, username, u);
|
||||
(*env)->ReleaseStringUTFChars(env, password, p);
|
||||
return code;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_dev_quicproquo_NativeBridge_nativeSend(
|
||||
JNIEnv* env, jclass cls,
|
||||
jlong handle, jstring recipient, jbyteArray message)
|
||||
{
|
||||
QpqHandle* h = (QpqHandle*)(uintptr_t)handle;
|
||||
const char* r = (*env)->GetStringUTFChars(env, recipient, NULL);
|
||||
jsize len = (*env)->GetArrayLength(env, message);
|
||||
jbyte* bytes = (*env)->GetByteArrayElements(env, message, NULL);
|
||||
|
||||
int code = qpq_send(h, r, (const uint8_t*)bytes, (size_t)len);
|
||||
|
||||
(*env)->ReleaseByteArrayElements(env, message, bytes, JNI_ABORT);
|
||||
(*env)->ReleaseStringUTFChars(env, recipient, r);
|
||||
return code;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_dev_quicproquo_NativeBridge_nativeReceive(
|
||||
JNIEnv* env, jclass cls,
|
||||
jlong handle, jint timeoutMs)
|
||||
{
|
||||
QpqHandle* h = (QpqHandle*)(uintptr_t)handle;
|
||||
char* json = NULL;
|
||||
int code = qpq_receive(h, (uint32_t)timeoutMs, &json);
|
||||
|
||||
if (code != 0 || json == NULL) {
|
||||
if (json) qpq_free_string(json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jstring result = (*env)->NewStringUTF(env, json);
|
||||
qpq_free_string(json);
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_dev_quicproquo_NativeBridge_nativeDisconnect(
|
||||
JNIEnv* env, jclass cls, jlong handle)
|
||||
{
|
||||
QpqHandle* h = (QpqHandle*)(uintptr_t)handle;
|
||||
if (h) {
|
||||
qpq_disconnect(h);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_dev_quicproquo_NativeBridge_nativeLastError(
|
||||
JNIEnv* env, jclass cls, jlong handle)
|
||||
{
|
||||
QpqHandle* h = (QpqHandle*)(uintptr_t)handle;
|
||||
const char* err = qpq_last_error(h);
|
||||
if (!err) return NULL;
|
||||
return (*env)->NewStringUTF(env, err);
|
||||
}
|
||||
52
sdks/kotlin/src/main/kotlin/dev/quicproquo/NativeBridge.kt
Normal file
52
sdks/kotlin/src/main/kotlin/dev/quicproquo/NativeBridge.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
package dev.quicproquo
|
||||
|
||||
/**
|
||||
* JNI bridge to libquicproquo_ffi native library.
|
||||
*
|
||||
* Load the library with:
|
||||
* ```kotlin
|
||||
* System.loadLibrary("quicproquo_ffi")
|
||||
* ```
|
||||
*
|
||||
* Or set `java.library.path` to the directory containing the shared object.
|
||||
*/
|
||||
internal object NativeBridge {
|
||||
|
||||
/** Status codes (mirrors crates/quicproquo-ffi/src/lib.rs). */
|
||||
const val QPQ_OK = 0
|
||||
const val QPQ_ERROR = 1
|
||||
const val QPQ_AUTH_FAILED = 2
|
||||
const val QPQ_TIMEOUT = 3
|
||||
const val QPQ_NOT_CONNECTED = 4
|
||||
|
||||
init {
|
||||
System.loadLibrary("quicproquo_ffi")
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a server. Returns a native handle (pointer as long),
|
||||
* or 0 on failure.
|
||||
*/
|
||||
@JvmStatic
|
||||
external fun nativeConnect(server: String, caCert: String, serverName: String): Long
|
||||
|
||||
/** Authenticate with OPAQUE. Returns a status code. */
|
||||
@JvmStatic
|
||||
external fun nativeLogin(handle: Long, username: String, password: String): Int
|
||||
|
||||
/** Send a message. Returns a status code. */
|
||||
@JvmStatic
|
||||
external fun nativeSend(handle: Long, recipient: String, message: ByteArray): Int
|
||||
|
||||
/** Receive messages. Returns JSON string or null. Throws on error. */
|
||||
@JvmStatic
|
||||
external fun nativeReceive(handle: Long, timeoutMs: Int): String?
|
||||
|
||||
/** Disconnect and free the handle. */
|
||||
@JvmStatic
|
||||
external fun nativeDisconnect(handle: Long)
|
||||
|
||||
/** Get last error message, or null. */
|
||||
@JvmStatic
|
||||
external fun nativeLastError(handle: Long): String?
|
||||
}
|
||||
105
sdks/kotlin/src/main/kotlin/dev/quicproquo/QpqClient.kt
Normal file
105
sdks/kotlin/src/main/kotlin/dev/quicproquo/QpqClient.kt
Normal file
@@ -0,0 +1,105 @@
|
||||
package dev.quicproquo
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* High-level quicproquo client for Android/JVM.
|
||||
*
|
||||
* Wraps `libquicproquo_ffi` via JNI to provide a Kotlin-native API.
|
||||
*
|
||||
* ```kotlin
|
||||
* val client = QpqClient("127.0.0.1:5001", caCertPath = "ca.pem")
|
||||
* client.login("alice", "secret")
|
||||
* client.send("bob", "hello".toByteArray())
|
||||
* val messages = client.receive(timeoutMs = 5000)
|
||||
* client.close()
|
||||
* ```
|
||||
*/
|
||||
class QpqClient(
|
||||
server: String,
|
||||
caCertPath: String,
|
||||
serverName: String = server.substringBefore(":")
|
||||
) : Closeable {
|
||||
|
||||
@Volatile
|
||||
private var handle: Long
|
||||
|
||||
private val gson = Gson()
|
||||
|
||||
init {
|
||||
val h = NativeBridge.nativeConnect(server, caCertPath, serverName)
|
||||
if (h == 0L) {
|
||||
throw QpqException("qpq_connect failed for $server")
|
||||
}
|
||||
handle = h
|
||||
}
|
||||
|
||||
/** Whether the client is connected. */
|
||||
val isConnected: Boolean
|
||||
get() = handle != 0L
|
||||
|
||||
/**
|
||||
* Authenticate with OPAQUE credentials.
|
||||
*
|
||||
* @throws QpqAuthException on bad credentials.
|
||||
*/
|
||||
fun login(username: String, password: String) {
|
||||
checkConnected()
|
||||
val code = NativeBridge.nativeLogin(handle, username, password)
|
||||
checkStatus(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a recipient by username.
|
||||
*
|
||||
* @param recipient Recipient username.
|
||||
* @param message Message payload (arbitrary bytes).
|
||||
*/
|
||||
fun send(recipient: String, message: ByteArray) {
|
||||
checkConnected()
|
||||
val code = NativeBridge.nativeSend(handle, recipient, message)
|
||||
checkStatus(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive pending messages, blocking up to [timeoutMs] milliseconds.
|
||||
*
|
||||
* @return List of message strings (UTF-8).
|
||||
*/
|
||||
fun receive(timeoutMs: Int = 5000): List<String> {
|
||||
checkConnected()
|
||||
val json = NativeBridge.nativeReceive(handle, timeoutMs) ?: return emptyList()
|
||||
val listType = object : TypeToken<List<String>>() {}.type
|
||||
return gson.fromJson(json, listType)
|
||||
}
|
||||
|
||||
/** Disconnect from the server and release resources. */
|
||||
override fun close() {
|
||||
val h = handle
|
||||
if (h != 0L) {
|
||||
handle = 0L
|
||||
NativeBridge.nativeDisconnect(h)
|
||||
}
|
||||
}
|
||||
|
||||
/** Alias for [close]. */
|
||||
fun disconnect() = close()
|
||||
|
||||
private fun checkConnected() {
|
||||
if (handle == 0L) throw QpqNotConnectedException()
|
||||
}
|
||||
|
||||
private fun checkStatus(code: Int) {
|
||||
if (code == NativeBridge.QPQ_OK) return
|
||||
|
||||
val msg = NativeBridge.nativeLastError(handle) ?: "unknown error"
|
||||
throw when (code) {
|
||||
NativeBridge.QPQ_AUTH_FAILED -> QpqAuthException(msg)
|
||||
NativeBridge.QPQ_TIMEOUT -> QpqTimeoutException(msg)
|
||||
NativeBridge.QPQ_NOT_CONNECTED -> QpqNotConnectedException()
|
||||
else -> QpqException(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
sdks/kotlin/src/main/kotlin/dev/quicproquo/QpqError.kt
Normal file
14
sdks/kotlin/src/main/kotlin/dev/quicproquo/QpqError.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package dev.quicproquo
|
||||
|
||||
/** Base exception for quicproquo SDK errors. */
|
||||
open class QpqException(message: String, cause: Throwable? = null) :
|
||||
RuntimeException(message, cause)
|
||||
|
||||
/** OPAQUE authentication failed (bad credentials). */
|
||||
class QpqAuthException(message: String) : QpqException(message)
|
||||
|
||||
/** Operation timed out. */
|
||||
class QpqTimeoutException(message: String) : QpqException(message)
|
||||
|
||||
/** Client is not connected. */
|
||||
class QpqNotConnectedException : QpqException("not connected")
|
||||
Reference in New Issue
Block a user