diff --git a/rust/src/config/mod.rs b/rust/src/config/mod.rs index d79d12a83a..a300e2034b 100644 --- a/rust/src/config/mod.rs +++ b/rust/src/config/mod.rs @@ -48,6 +48,7 @@ pub struct AuthConfig { #[derive(FromPyObject, Clone)] pub struct ServerConfig { pub msc4140_enabled: bool, + pub public_baseurl: String, } #[derive(FromPyObject, Clone)] diff --git a/rust/src/handlers/mod.rs b/rust/src/handlers/mod.rs index 856ce7f6e4..b5ad872bc1 100644 --- a/rust/src/handlers/mod.rs +++ b/rust/src/handlers/mod.rs @@ -21,8 +21,7 @@ use pyo3::{ Bound, PyResult, Python, }; -use crate::config::SynapseHomeServerConfig; -use crate::runtime::RustRuntime; +use crate::homeserver::HomeServer; use crate::storage::db::python_db_pool::PythonDatabasePoolWrapper; use crate::storage::store::Store; @@ -37,22 +36,17 @@ struct RustHandlers { impl RustHandlers { #[new] #[pyo3(signature = (homeserver))] - pub fn py_new(py: Python<'_>, homeserver: &Bound<'_, PyAny>) -> PyResult { - let config: SynapseHomeServerConfig = homeserver.getattr("config")?.extract()?; + pub fn py_new(py: Python<'_>, homeserver: HomeServer) -> PyResult { + let config = homeserver.config(py)?; // The per-homeserver Rust state, which gives us the tokio runtime // and the Twisted reactor. - let runtime: Bound<'_, RustRuntime> = - homeserver.call_method0("get_rust_runtime")?.extract()?; - let runtime = Arc::clone(runtime.get().inner()); + let runtime = homeserver.rust_runtime(py)?; - // hs.get_datastores().main.db_pool - let db_pool_py = homeserver - .call_method0("get_datastores")? - .getattr("main")? - .getattr("db_pool")?; - let db_pool = - PythonDatabasePoolWrapper::new(&db_pool_py, runtime.reactor().clone_ref(py))?; + let db_pool = PythonDatabasePoolWrapper::new( + &homeserver.main_database_pool(py)?, + runtime.reactor().clone_ref(py), + )?; // Store is shared across all of the handlers so let's use an `Arc` let store = Arc::new(Store { diff --git a/rust/src/homeserver.rs b/rust/src/homeserver.rs new file mode 100644 index 0000000000..d34ec00e13 --- /dev/null +++ b/rust/src/homeserver.rs @@ -0,0 +1,77 @@ +/* + * This file is licensed under the Affero General Public License (AGPL) version 3. + * + * Copyright (C) 2026 Element Creations Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * See the GNU Affero General Public License for more details: + * . + * + */ + +//! A typed wrapper around the Python `HomeServer`. + +use std::sync::Arc; + +use pyo3::{intern, prelude::*}; + +use crate::config::SynapseHomeServerConfig; +use crate::runtime::{RustRuntime, RustRuntimeInner}; + +/// The Python `HomeServer`, as seen from Rust. +/// +/// Like [`crate::reactor::Reactor`], this is a typed facade over a foreign +/// Python object: it does no validation of the object it wraps (the Python +/// side is type-checked by mypy), but it is the single place that names the +/// `HomeServer` API surface that Rust code depends on. +pub struct HomeServer(Py); + +impl<'a, 'py> FromPyObject<'a, 'py> for HomeServer { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult { + Ok(HomeServer(obj.to_owned().unbind())) + } +} + +impl HomeServer { + /// The per-homeserver Rust state (`hs.get_rust_runtime()`), which gives + /// access to the tokio runtime and the reactor. + pub fn rust_runtime(&self, py: Python<'_>) -> PyResult> { + let runtime: Bound<'_, RustRuntime> = self + .0 + .bind(py) + .call_method0(intern!(py, "get_rust_runtime"))? + .extract()?; + + Ok(Arc::clone(runtime.get().inner())) + } + + /// The Rust-side view of `hs.config`. + pub fn config(&self, py: Python<'_>) -> PyResult { + self.0.bind(py).getattr(intern!(py, "config"))?.extract() + } + + /// The Synapse `Clock` (`hs.get_clock()`). + // TODO: give the clock a typed wrapper of its own. + pub fn clock(&self, py: Python<'_>) -> PyResult> { + Ok(self + .0 + .bind(py) + .call_method0(intern!(py, "get_clock"))? + .unbind()) + } + + /// The main database pool (`hs.get_datastores().main.db_pool`). + pub fn main_database_pool<'py>(&self, py: Python<'py>) -> PyResult> { + self.0 + .bind(py) + .call_method0(intern!(py, "get_datastores"))? + .getattr(intern!(py, "main"))? + .getattr(intern!(py, "db_pool")) + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 90c2d85f6c..7da779fed0 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -12,6 +12,7 @@ pub mod duration; pub mod errors; pub mod events; pub mod handlers; +pub mod homeserver; pub mod http; pub mod http_client; pub mod identifier; diff --git a/rust/src/msc4388_rendezvous/mod.rs b/rust/src/msc4388_rendezvous/mod.rs index 2f1b004ec1..943b147a30 100644 --- a/rust/src/msc4388_rendezvous/mod.rs +++ b/rust/src/msc4388_rendezvous/mod.rs @@ -21,7 +21,7 @@ use http::StatusCode; use pyo3::{ pyclass, pymethods, types::{PyAnyMethods, PyModule, PyModuleMethods}, - Bound, IntoPyObject, Py, PyAny, PyResult, Python, + Bound, Py, PyAny, PyResult, Python, }; use serde::Deserialize; use ulid::Ulid; @@ -30,9 +30,9 @@ use self::session::Session; use crate::{ duration::SynapseDuration, errors::{NotFoundError, SynapseError}, + homeserver::HomeServer, http::http_request_from_twisted, msc4388_rendezvous::session::{GetResponse, PostResponse, PutResponse}, - UnwrapInfallible, }; mod session; @@ -92,25 +92,21 @@ impl MSC4388RendezvousHandler { #[pyo3(signature = (homeserver, /, soft_limit=100, hard_limit=200,max_content_length=4*1024, eviction_interval=60*1000, ttl=2*60*1000))] fn new( py: Python<'_>, - homeserver: &Bound<'_, PyAny>, + homeserver: HomeServer, soft_limit: usize, hard_limit: usize, max_content_length: u64, eviction_interval: u64, ttl: u64, ) -> PyResult> { - let clock = homeserver - .call_method0("get_clock")? - .into_pyobject(py) - .unwrap_infallible() - .unbind(); + let clock = homeserver.clock(py)?; // Construct a Python object so that we can get a reference to the // evict method and schedule it to run. let self_ = Py::new( py, Self { - clock, + clock: clock.clone_ref(py), sessions: BTreeMap::new(), soft_limit, hard_limit, @@ -122,11 +118,9 @@ impl MSC4388RendezvousHandler { let eviction_duration = SynapseDuration::from_milliseconds(eviction_interval); let evict = self_.getattr(py, "_evict")?; - homeserver.call_method0("get_clock")?.call_method( - "looping_call", - (evict, &eviction_duration), - None, - )?; + clock + .bind(py) + .call_method("looping_call", (evict, &eviction_duration), None)?; Ok(self_) } diff --git a/rust/src/reactor.rs b/rust/src/reactor.rs index 999103d51e..2ac4d1a3be 100644 --- a/rust/src/reactor.rs +++ b/rust/src/reactor.rs @@ -15,36 +15,20 @@ //! A typed wrapper around the Twisted reactor. -use pyo3::{call::PyCallArgs, exceptions::PyTypeError, intern, prelude::*}; - -/// The reactor methods Rust code relies on. -/// -/// Extracting a [`Reactor`] from a Python object fails if any of these are -/// missing, so mistakes surface as a `TypeError` at the FFI boundary rather -/// than as an `AttributeError` on a tokio worker thread later. -const REQUIRED_METHODS: &[&str] = &["callFromThread", "addSystemEventTrigger"]; +use pyo3::{call::PyCallArgs, intern, prelude::*}; /// The Twisted reactor, as seen from Rust. /// -/// This is not a static guarantee that the object behaves like a reactor (it -/// is a foreign Python object), but the duck type is checked once at -/// extraction, and this module is the single place that names the Twisted -/// API surface that Rust code depends on. +/// A typed facade over a foreign Python object: it does no validation of the +/// object it wraps (the Python side is type-checked by mypy), but it is the +/// single place that names the Twisted API surface that Rust code depends +/// on. pub struct Reactor(Py); impl<'a, 'py> FromPyObject<'a, 'py> for Reactor { type Error = PyErr; fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult { - for name in REQUIRED_METHODS { - if !obj.hasattr(*name)? { - return Err(PyTypeError::new_err(format!( - "expected a Twisted reactor, but {} has no `{name}` method", - obj.get_type() - ))); - } - } - Ok(Reactor(obj.to_owned().unbind())) } } diff --git a/rust/src/rendezvous/mod.rs b/rust/src/rendezvous/mod.rs index 9a6da9fcc3..54a2d259bb 100644 --- a/rust/src/rendezvous/mod.rs +++ b/rust/src/rendezvous/mod.rs @@ -29,7 +29,7 @@ use pyo3::{ exceptions::PyValueError, pyclass, pymethods, types::{PyAnyMethods, PyModule, PyModuleMethods}, - Bound, IntoPyObject, Py, PyAny, PyResult, Python, + Bound, Py, PyAny, PyResult, Python, }; use ulid::Ulid; @@ -37,8 +37,8 @@ use self::session::Session; use crate::{ duration::SynapseDuration, errors::{NotFoundError, SynapseError}, + homeserver::HomeServer, http::{http_request_from_twisted, http_response_to_twisted, HeaderMapPyExt}, - UnwrapInfallible, }; mod session; @@ -113,25 +113,17 @@ impl RendezvousHandler { #[pyo3(signature = (homeserver, /, capacity=100, max_content_length=4*1024, eviction_interval=60*1000, ttl=60*1000))] fn new( py: Python<'_>, - homeserver: &Bound<'_, PyAny>, + homeserver: HomeServer, capacity: usize, max_content_length: u64, eviction_interval: u64, ttl: u64, ) -> PyResult> { - let base: String = homeserver - .getattr("config")? - .getattr("server")? - .getattr("public_baseurl")? - .extract()?; + let base = homeserver.config(py)?.server.public_baseurl; let base = Uri::try_from(format!("{base}_synapse/client/rendezvous")) .map_err(|_| PyValueError::new_err("Invalid base URI"))?; - let clock = homeserver - .call_method0("get_clock")? - .into_pyobject(py) - .unwrap_infallible() - .unbind(); + let clock = homeserver.clock(py)?; let eviction_duration = SynapseDuration::from_milliseconds(eviction_interval); @@ -141,7 +133,7 @@ impl RendezvousHandler { py, Self { base, - clock, + clock: clock.clone_ref(py), sessions: BTreeMap::new(), capacity, max_content_length, @@ -150,11 +142,9 @@ impl RendezvousHandler { )?; let evict = self_.getattr(py, "_evict")?; - homeserver.call_method0("get_clock")?.call_method( - "looping_call", - (evict, &eviction_duration), - None, - )?; + clock + .bind(py) + .call_method("looping_call", (evict, &eviction_duration), None)?; Ok(self_) }