feat: add Python (PyO3) and Ruby (Magnus) native bindings
Introduces three crates: - quicnprotochat-bindings: shared Rust API returning structured data instead of printing to stdout; explicit per-call auth (no global OnceLock); QPCE state files fully interoperable with the CLI. - quicnprotochat-python: PyO3 0.22 extension; GIL released during all blocking QUIC calls via py.allow_threads. - quicnprotochat-ruby: Magnus 0.7 extension; Rakefile build task. Core/proto crates referenced via git dep on the main repo.
This commit is contained in:
14
crates/quicnprotochat-ruby/Cargo.toml
Normal file
14
crates/quicnprotochat-ruby/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "quicnprotochat-ruby"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Ruby bindings for quicnprotochat (Magnus)."
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
name = "quicnprotochat_ruby"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
quicnprotochat-bindings = { path = "../quicnprotochat-bindings" }
|
||||
magnus = { version = "0.7" }
|
||||
39
crates/quicnprotochat-ruby/Rakefile
Normal file
39
crates/quicnprotochat-ruby/Rakefile
Normal file
@@ -0,0 +1,39 @@
|
||||
require "fileutils"
|
||||
|
||||
WORKSPACE_ROOT = File.expand_path("../..", __dir__)
|
||||
TARGET_DIR = File.join(WORKSPACE_ROOT, "target")
|
||||
LIB_DIR = File.join(__dir__, "lib")
|
||||
|
||||
# Detect the shared library extension for the current platform.
|
||||
SO_EXT = case RUBY_PLATFORM
|
||||
when /darwin/ then "dylib"
|
||||
when /mingw|mswin/ then "dll"
|
||||
else "so"
|
||||
end
|
||||
|
||||
LIB_SRC = File.join(TARGET_DIR, "release", "libquicnprotochat_ruby.#{SO_EXT}")
|
||||
LIB_DEST = File.join(LIB_DIR, "quicnprotochat_ruby.#{SO_EXT}")
|
||||
|
||||
desc "Build the native extension (release)"
|
||||
task :build do
|
||||
sh "cargo build --release --manifest-path #{File.join(__dir__, "Cargo.toml")}"
|
||||
FileUtils.mkdir_p(LIB_DIR)
|
||||
FileUtils.cp(LIB_SRC, LIB_DEST)
|
||||
puts "Copied #{LIB_DEST}"
|
||||
end
|
||||
|
||||
desc "Build the native extension (debug)"
|
||||
task :build_dev do
|
||||
sh "cargo build --manifest-path #{File.join(__dir__, "Cargo.toml")}"
|
||||
lib_src = File.join(TARGET_DIR, "debug", "libquicnprotochat_ruby.#{SO_EXT}")
|
||||
FileUtils.mkdir_p(LIB_DIR)
|
||||
FileUtils.cp(lib_src, LIB_DEST)
|
||||
puts "Copied #{LIB_DEST} (debug)"
|
||||
end
|
||||
|
||||
desc "Remove build artefacts"
|
||||
task :clean do
|
||||
FileUtils.rm_f(LIB_DEST)
|
||||
end
|
||||
|
||||
task default: :build
|
||||
170
crates/quicnprotochat-ruby/src/lib.rs
Normal file
170
crates/quicnprotochat-ruby/src/lib.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use magnus::{
|
||||
class, define_module, exception, function, method,
|
||||
prelude::*,
|
||||
Error, Ruby,
|
||||
};
|
||||
use quicnprotochat_bindings::Client;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
fn to_rb(e: anyhow::Error) -> Error {
|
||||
Error::new(exception::runtime_error(), e.to_string())
|
||||
}
|
||||
|
||||
// ── RbClient ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Ruby wrapper around `Client`. Wrapped in `Arc<Mutex<>>` so that Ruby's GC
|
||||
/// can safely reference-count it while the GVL serialises all calls.
|
||||
#[magnus::wrap(class = "QuicNProtoChat::Client", free_immediately, size)]
|
||||
struct RbClient(Arc<Mutex<Client>>);
|
||||
|
||||
impl RbClient {
|
||||
fn new(
|
||||
ruby: &Ruby,
|
||||
server: String,
|
||||
ca_cert: String,
|
||||
server_name: String,
|
||||
state_path: String,
|
||||
access_token: String,
|
||||
state_password: Option<String>,
|
||||
device_id: Option<String>,
|
||||
) -> Result<Self, Error> {
|
||||
let inner = Client::new(
|
||||
&server,
|
||||
&ca_cert,
|
||||
&server_name,
|
||||
&state_path,
|
||||
&access_token,
|
||||
state_password,
|
||||
device_id,
|
||||
)
|
||||
.map_err(to_rb)?;
|
||||
Ok(RbClient(Arc::new(Mutex::new(inner))))
|
||||
}
|
||||
|
||||
fn whoami(ruby: &Ruby, rb_self: &RbClient) -> Result<magnus::RHash, Error> {
|
||||
let info = rb_self.0.lock().unwrap().whoami().map_err(to_rb)?;
|
||||
let h = ruby.hash_new();
|
||||
h.aset(ruby.str_new("identity_key"), ruby.str_new(&info.identity_key))?;
|
||||
h.aset(ruby.str_new("fingerprint"), ruby.str_new(&info.fingerprint))?;
|
||||
h.aset(ruby.str_new("hybrid_key"), info.hybrid_key)?;
|
||||
h.aset(ruby.str_new("has_group"), info.has_group)?;
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
fn health(ruby: &Ruby, rb_self: &RbClient) -> Result<magnus::RHash, Error> {
|
||||
let info = rb_self.0.lock().unwrap().health().map_err(to_rb)?;
|
||||
let h = ruby.hash_new();
|
||||
h.aset(ruby.str_new("status"), ruby.str_new(&info.status))?;
|
||||
h.aset(ruby.str_new("rtt_ms"), info.rtt_ms)?;
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
fn register_user(
|
||||
_ruby: &Ruby,
|
||||
rb_self: &RbClient,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<(), Error> {
|
||||
rb_self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.register_user(&username, &password)
|
||||
.map_err(to_rb)
|
||||
}
|
||||
|
||||
fn login(
|
||||
_ruby: &Ruby,
|
||||
rb_self: &RbClient,
|
||||
username: String,
|
||||
password: String,
|
||||
) -> Result<String, Error> {
|
||||
rb_self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.login(&username, &password)
|
||||
.map_err(to_rb)
|
||||
}
|
||||
|
||||
fn register_state(_ruby: &Ruby, rb_self: &RbClient) -> Result<String, Error> {
|
||||
rb_self.0.lock().unwrap().register_state().map_err(to_rb)
|
||||
}
|
||||
|
||||
fn check_key(_ruby: &Ruby, rb_self: &RbClient, peer_hex: String) -> Result<bool, Error> {
|
||||
rb_self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.check_key(&peer_hex)
|
||||
.map_err(to_rb)
|
||||
}
|
||||
|
||||
fn create_group(_ruby: &Ruby, rb_self: &RbClient, group_id: String) -> Result<(), Error> {
|
||||
rb_self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.create_group(&group_id)
|
||||
.map_err(to_rb)
|
||||
}
|
||||
|
||||
fn invite(_ruby: &Ruby, rb_self: &RbClient, peer_hex: String) -> Result<(), Error> {
|
||||
rb_self.0.lock().unwrap().invite(&peer_hex).map_err(to_rb)
|
||||
}
|
||||
|
||||
fn join(_ruby: &Ruby, rb_self: &RbClient) -> Result<(), Error> {
|
||||
rb_self.0.lock().unwrap().join().map_err(to_rb)
|
||||
}
|
||||
|
||||
fn send_message(
|
||||
_ruby: &Ruby,
|
||||
rb_self: &RbClient,
|
||||
peer_hex: String,
|
||||
text: String,
|
||||
) -> Result<(), Error> {
|
||||
rb_self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.send_message(&peer_hex, &text)
|
||||
.map_err(to_rb)
|
||||
}
|
||||
|
||||
fn recv(ruby: &Ruby, rb_self: &RbClient, wait_ms: u64) -> Result<magnus::RArray, Error> {
|
||||
let msgs = rb_self.0.lock().unwrap().recv(wait_ms).map_err(to_rb)?;
|
||||
let arr = ruby.ary_new_capa(msgs.len());
|
||||
for m in msgs {
|
||||
let h = ruby.hash_new();
|
||||
h.aset(ruby.str_new("plaintext"), ruby.str_new(&m.plaintext))?;
|
||||
arr.push(h)?;
|
||||
}
|
||||
Ok(arr)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[magnus::init]
|
||||
fn init(ruby: &Ruby) -> Result<(), Error> {
|
||||
let module = ruby.define_module("QuicNProtoChat")?;
|
||||
let cls = module.define_class("Client", ruby.class_object())?;
|
||||
|
||||
// QuicNProtoChat::Client.new(server, ca_cert, server_name, state_path,
|
||||
// access_token, state_password=nil, device_id=nil)
|
||||
cls.define_singleton_method("new", method!(RbClient::new, 7))?;
|
||||
|
||||
cls.define_method("whoami", method!(RbClient::whoami, 0))?;
|
||||
cls.define_method("health", method!(RbClient::health, 0))?;
|
||||
cls.define_method("register_user", method!(RbClient::register_user, 2))?;
|
||||
cls.define_method("login", method!(RbClient::login, 2))?;
|
||||
cls.define_method("register_state", method!(RbClient::register_state, 0))?;
|
||||
cls.define_method("check_key", method!(RbClient::check_key, 1))?;
|
||||
cls.define_method("create_group", method!(RbClient::create_group, 1))?;
|
||||
cls.define_method("invite", method!(RbClient::invite, 1))?;
|
||||
cls.define_method("join", method!(RbClient::join, 0))?;
|
||||
cls.define_method("send_message", method!(RbClient::send_message, 2))?;
|
||||
cls.define_method("recv", method!(RbClient::recv, 1))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user