chore: rename quicproquo → quicprochat in Rust workspace
Rename all crate directories, package names, binary names, proto package/module paths, ALPN strings, env var prefixes, config filenames, mDNS service names, and plugin ABI symbols from quicproquo/qpq to quicprochat/qpc.
This commit is contained in:
192
sdks/python/quicprochat/ffi.py
Normal file
192
sdks/python/quicprochat/ffi.py
Normal file
@@ -0,0 +1,192 @@
|
||||
"""CFFI bindings to ``libquicproquo_ffi`` (the Rust C FFI layer).
|
||||
|
||||
This module loads the shared library and exposes a synchronous Python API
|
||||
that mirrors the C functions in ``crates/quicproquo-ffi/src/lib.rs``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import cffi
|
||||
|
||||
from quicproquo.types import (
|
||||
QpqError,
|
||||
AuthError,
|
||||
TimeoutError,
|
||||
ConnectionError,
|
||||
)
|
||||
|
||||
# Status codes (must match crates/quicproquo-ffi/src/lib.rs).
|
||||
QPQ_OK = 0
|
||||
QPQ_ERROR = 1
|
||||
QPQ_AUTH_FAILED = 2
|
||||
QPQ_TIMEOUT = 3
|
||||
QPQ_NOT_CONNECTED = 4
|
||||
|
||||
_CDEFS = """
|
||||
typedef struct QpqHandle QpqHandle;
|
||||
|
||||
QpqHandle* qpq_connect(const char* server, const char* ca_cert, const char* server_name);
|
||||
int qpq_login(QpqHandle* handle, const char* username, const char* password);
|
||||
int qpq_send(QpqHandle* handle, const char* recipient, const uint8_t* message, size_t message_len);
|
||||
int qpq_receive(QpqHandle* handle, uint32_t timeout_ms, char** out_json);
|
||||
void qpq_disconnect(QpqHandle* handle);
|
||||
const char* qpq_last_error(const QpqHandle* handle);
|
||||
void qpq_free_string(char* ptr);
|
||||
"""
|
||||
|
||||
_ffi = cffi.FFI()
|
||||
_ffi.cdef(_CDEFS)
|
||||
_lib: Optional[object] = None
|
||||
|
||||
|
||||
def _load_lib() -> object:
|
||||
"""Load the shared library, searching common paths."""
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
|
||||
search_paths = [
|
||||
# Explicit environment variable.
|
||||
os.environ.get("QPQ_LIB_PATH", ""),
|
||||
# Common cargo build output locations.
|
||||
str(Path(__file__).resolve().parents[3] / "target" / "release" / "libquicproquo_ffi.so"),
|
||||
str(Path(__file__).resolve().parents[3] / "target" / "debug" / "libquicproquo_ffi.so"),
|
||||
# macOS dylib.
|
||||
str(
|
||||
Path(__file__).resolve().parents[3]
|
||||
/ "target"
|
||||
/ "release"
|
||||
/ "libquicproquo_ffi.dylib"
|
||||
),
|
||||
str(
|
||||
Path(__file__).resolve().parents[3]
|
||||
/ "target"
|
||||
/ "debug"
|
||||
/ "libquicproquo_ffi.dylib"
|
||||
),
|
||||
# System library path.
|
||||
"libquicproquo_ffi.so",
|
||||
]
|
||||
|
||||
for path in search_paths:
|
||||
if not path:
|
||||
continue
|
||||
try:
|
||||
_lib = _ffi.dlopen(path)
|
||||
return _lib
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
raise OSError(
|
||||
"Could not find libquicproquo_ffi. Set QPQ_LIB_PATH or build with "
|
||||
"`cargo build --release -p quicproquo-ffi`."
|
||||
)
|
||||
|
||||
|
||||
def _check_error(handle: object, code: int) -> None:
|
||||
"""Raise an appropriate exception if the FFI call returned an error code."""
|
||||
if code == QPQ_OK:
|
||||
return
|
||||
|
||||
lib = _load_lib()
|
||||
err_ptr = lib.qpq_last_error(handle) # type: ignore[union-attr]
|
||||
msg = _ffi.string(err_ptr).decode("utf-8") if err_ptr != _ffi.NULL else "unknown error"
|
||||
|
||||
if code == QPQ_AUTH_FAILED:
|
||||
raise AuthError(msg)
|
||||
if code == QPQ_TIMEOUT:
|
||||
raise TimeoutError(msg)
|
||||
if code == QPQ_NOT_CONNECTED:
|
||||
raise ConnectionError(msg)
|
||||
raise QpqError(msg)
|
||||
|
||||
|
||||
class FfiTransport:
|
||||
"""Synchronous transport wrapping ``libquicproquo_ffi``.
|
||||
|
||||
Provides the same logical operations as ``QuicTransport`` but backed
|
||||
by the Rust client library through C FFI.
|
||||
|
||||
Usage::
|
||||
|
||||
ffi = FfiTransport.connect("127.0.0.1:5001", ca_cert="/path/to/ca.pem")
|
||||
ffi.login("alice", "password123")
|
||||
ffi.send("bob", b"hello")
|
||||
messages = ffi.receive(timeout_ms=5000)
|
||||
ffi.close()
|
||||
"""
|
||||
|
||||
def __init__(self, handle: object) -> None:
|
||||
self._handle = handle
|
||||
self._lib = _load_lib()
|
||||
|
||||
@staticmethod
|
||||
def connect(
|
||||
addr: str,
|
||||
*,
|
||||
ca_cert_path: str = "",
|
||||
server_name: str = "",
|
||||
) -> "FfiTransport":
|
||||
"""Connect to a qpq server via the Rust FFI layer."""
|
||||
lib = _load_lib()
|
||||
|
||||
server_c = _ffi.new("char[]", addr.encode("utf-8"))
|
||||
ca_c = _ffi.new("char[]", ca_cert_path.encode("utf-8"))
|
||||
|
||||
if not server_name:
|
||||
host = addr.split(":")[0]
|
||||
server_name = host
|
||||
sn_c = _ffi.new("char[]", server_name.encode("utf-8"))
|
||||
|
||||
handle = lib.qpq_connect(server_c, ca_c, sn_c) # type: ignore[union-attr]
|
||||
if handle == _ffi.NULL:
|
||||
raise ConnectionError(f"qpq_connect failed for {addr}")
|
||||
|
||||
return FfiTransport(handle)
|
||||
|
||||
def login(self, username: str, password: str) -> None:
|
||||
"""Authenticate with OPAQUE credentials."""
|
||||
u = _ffi.new("char[]", username.encode("utf-8"))
|
||||
p = _ffi.new("char[]", password.encode("utf-8"))
|
||||
code = self._lib.qpq_login(self._handle, u, p) # type: ignore[union-attr]
|
||||
_check_error(self._handle, code)
|
||||
|
||||
def send(self, recipient: str, message: bytes) -> None:
|
||||
"""Send a message to a recipient (by username)."""
|
||||
r = _ffi.new("char[]", recipient.encode("utf-8"))
|
||||
m = _ffi.new("uint8_t[]", message)
|
||||
code = self._lib.qpq_send(self._handle, r, m, len(message)) # type: ignore[union-attr]
|
||||
_check_error(self._handle, code)
|
||||
|
||||
def receive(self, timeout_ms: int = 5000) -> list[str]:
|
||||
"""Receive pending messages, blocking up to *timeout_ms*.
|
||||
|
||||
Returns a list of message strings (UTF-8).
|
||||
"""
|
||||
out = _ffi.new("char**")
|
||||
code = self._lib.qpq_receive(self._handle, timeout_ms, out) # type: ignore[union-attr]
|
||||
_check_error(self._handle, code)
|
||||
|
||||
if out[0] == _ffi.NULL:
|
||||
return []
|
||||
|
||||
json_str = _ffi.string(out[0]).decode("utf-8")
|
||||
self._lib.qpq_free_string(out[0]) # type: ignore[union-attr]
|
||||
return json.loads(json_str) # type: ignore[no-any-return]
|
||||
|
||||
def close(self) -> None:
|
||||
"""Disconnect and free the handle."""
|
||||
if self._handle is not None:
|
||||
self._lib.qpq_disconnect(self._handle) # type: ignore[union-attr]
|
||||
self._handle = None
|
||||
|
||||
def __enter__(self) -> "FfiTransport":
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
self.close()
|
||||
Reference in New Issue
Block a user