feat(sdk): add Java and Ruby SDK wrappers over C FFI

Java SDK: JNI bindings to libquicproquo_ffi with QpqClient class,
Gradle build, and exception hierarchy matching Kotlin SDK.

Ruby SDK: FFI gem wrapping libquicproquo_ffi with Client class,
block-form auto-disconnect, gemspec for RubyGems publishing,
and example script.
This commit is contained in:
2026-03-04 21:00:20 +01:00
parent 3f5a3a5ac8
commit 12717979ba
15 changed files with 619 additions and 0 deletions

50
sdks/java/README.md Normal file
View File

@@ -0,0 +1,50 @@
# QuicProQuo Java SDK
Java wrapper over `libquicproquo_ffi` via JNI for JVM and Android.
## Prerequisites
- JDK 17+
- `libquicproquo_ffi` built for the target platform
- JNI bridge compiled (shared with Kotlin SDK: `../kotlin/jni/`)
## Building
```sh
# Build Rust FFI library
cargo build --release -p quicproquo-ffi
# Build Java SDK
./gradlew build
```
## Usage
```java
import dev.quicproquo.QpqClient;
try (QpqClient client = new QpqClient("127.0.0.1:5001", "ca.pem")) {
client.login("alice", "secret");
client.send("bob", "hello".getBytes());
var messages = client.receive(5000);
messages.forEach(msg -> System.out.println("Received: " + msg));
}
```
## API
| Method | Description |
|---|---|
| `new QpqClient(server, caCertPath)` | 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 |
## Structure
- `src/main/java/dev/quicproquo/QpqClient.java` -- High-level client
- `src/main/java/dev/quicproquo/NativeBridge.java` -- JNI declarations
- JNI C bridge shared with Kotlin SDK at `../kotlin/jni/`

View File

@@ -0,0 +1,24 @@
plugins {
java
}
group = "dev.quicproquo"
version = "0.1.0"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.google.code.gson:gson:2.10.1")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
tasks.test {
useJUnitPlatform()
}

View File

@@ -0,0 +1,30 @@
package dev.quicproquo;
/**
* JNI bridge to libquicproquo_ffi native library.
*
* <p>Load the library with:
* <pre>{@code
* System.loadLibrary("quicproquo_ffi");
* }</pre>
*/
final class NativeBridge {
static final int QPQ_OK = 0;
static final int QPQ_ERROR = 1;
static final int QPQ_AUTH_FAILED = 2;
static final int QPQ_TIMEOUT = 3;
static final int QPQ_NOT_CONNECTED = 4;
static {
System.loadLibrary("quicproquo_ffi");
}
static native long nativeConnect(String server, String caCert, String serverName);
static native int nativeLogin(long handle, String username, String password);
static native int nativeSend(long handle, String recipient, byte[] message);
static native String nativeReceive(long handle, int timeoutMs);
static native void nativeDisconnect(long handle);
static native String nativeLastError(long handle);
private NativeBridge() {}
}

View File

@@ -0,0 +1,6 @@
package dev.quicproquo;
/** OPAQUE authentication failed (bad credentials). */
public class QpqAuthException extends QpqException {
public QpqAuthException(String message) { super(message); }
}

View File

@@ -0,0 +1,127 @@
package dev.quicproquo;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
/**
* High-level quicproquo client for JVM.
*
* <p>Wraps {@code libquicproquo_ffi} via JNI.
*
* <pre>{@code
* try (QpqClient client = new QpqClient("127.0.0.1:5001", "ca.pem")) {
* client.login("alice", "secret");
* client.send("bob", "hello".getBytes());
* List<String> messages = client.receive(5000);
* }
* }</pre>
*/
public final class QpqClient implements AutoCloseable {
private volatile long handle;
private final Gson gson = new Gson();
private static final Type STRING_LIST_TYPE =
new TypeToken<List<String>>() {}.getType();
/**
* Connect to a quicproquo server.
*
* @param server server address as {@code host:port}
* @param caCertPath path to PEM-encoded CA certificate
*/
public QpqClient(String server, String caCertPath) {
this(server, caCertPath, server.split(":")[0]);
}
/**
* Connect to a quicproquo server with explicit TLS server name.
*
* @param server server address as {@code host:port}
* @param caCertPath path to PEM-encoded CA certificate
* @param serverName TLS SNI server name
*/
public QpqClient(String server, String caCertPath, String serverName) {
long h = NativeBridge.nativeConnect(server, caCertPath, serverName);
if (h == 0) {
throw new QpqException("qpq_connect failed for " + server);
}
this.handle = h;
}
/** Whether the client is connected. */
public boolean isConnected() {
return handle != 0;
}
/**
* Authenticate with OPAQUE credentials.
*
* @throws QpqAuthException on bad credentials
*/
public void login(String username, String password) {
checkConnected();
int 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)
*/
public void send(String recipient, byte[] message) {
checkConnected();
int code = NativeBridge.nativeSend(handle, recipient, message);
checkStatus(code);
}
/**
* Receive pending messages, blocking up to {@code timeoutMs} milliseconds.
*
* @return list of message strings (UTF-8)
*/
public List<String> receive(int timeoutMs) {
checkConnected();
String json = NativeBridge.nativeReceive(handle, timeoutMs);
if (json == null) return Collections.emptyList();
return gson.fromJson(json, STRING_LIST_TYPE);
}
/** Disconnect from the server and release resources. */
@Override
public void close() {
long h = handle;
if (h != 0) {
handle = 0;
NativeBridge.nativeDisconnect(h);
}
}
/** Alias for {@link #close()}. */
public void disconnect() {
close();
}
private void checkConnected() {
if (handle == 0) throw new QpqException("not connected");
}
private void checkStatus(int code) {
if (code == NativeBridge.QPQ_OK) return;
String msg = NativeBridge.nativeLastError(handle);
if (msg == null) msg = "unknown error";
switch (code) {
case NativeBridge.QPQ_AUTH_FAILED:
throw new QpqAuthException(msg);
case NativeBridge.QPQ_TIMEOUT:
throw new QpqTimeoutException(msg);
case NativeBridge.QPQ_NOT_CONNECTED:
throw new QpqException("not connected: " + msg);
default:
throw new QpqException(msg);
}
}
}

View File

@@ -0,0 +1,7 @@
package dev.quicproquo;
/** Base exception for quicproquo SDK errors. */
public class QpqException extends RuntimeException {
public QpqException(String message) { super(message); }
public QpqException(String message, Throwable cause) { super(message, cause); }
}

View File

@@ -0,0 +1,6 @@
package dev.quicproquo;
/** Operation timed out. */
public class QpqTimeoutException extends QpqException {
public QpqTimeoutException(String message) { super(message); }
}