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:
50
sdks/java/README.md
Normal file
50
sdks/java/README.md
Normal 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/`
|
||||
24
sdks/java/build.gradle.kts
Normal file
24
sdks/java/build.gradle.kts
Normal 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()
|
||||
}
|
||||
30
sdks/java/src/main/java/dev/quicproquo/NativeBridge.java
Normal file
30
sdks/java/src/main/java/dev/quicproquo/NativeBridge.java
Normal 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() {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package dev.quicproquo;
|
||||
|
||||
/** OPAQUE authentication failed (bad credentials). */
|
||||
public class QpqAuthException extends QpqException {
|
||||
public QpqAuthException(String message) { super(message); }
|
||||
}
|
||||
127
sdks/java/src/main/java/dev/quicproquo/QpqClient.java
Normal file
127
sdks/java/src/main/java/dev/quicproquo/QpqClient.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
sdks/java/src/main/java/dev/quicproquo/QpqException.java
Normal file
7
sdks/java/src/main/java/dev/quicproquo/QpqException.java
Normal 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); }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package dev.quicproquo;
|
||||
|
||||
/** Operation timed out. */
|
||||
public class QpqTimeoutException extends QpqException {
|
||||
public QpqTimeoutException(String message) { super(message); }
|
||||
}
|
||||
Reference in New Issue
Block a user