Add Platform trait.

I did some more thinking about the whole trait/boundary stuff and reallized that we don't need the GUI to handle the generic-ness of a trait object since only 1 platform will ever be used in a binary. What do you think of the following:

1. Define a platform trait
2. Each platform defines a zero-sized struct implementing the trait (ex `WebPlatform`).
3. Create an ifdef'd type alias on those structs:

```
     // gui/src/platform/mod.rs
     #[cfg(feature = "web")]
     pub type CurrentPlatform = web::WebPlatform;
     #[cfg(feature = "desktop")]
     pub type CurrentPlatform = desktop::DesktopPlatform;
     #[cfg(feature = "mobile")]
     pub type CurrentPlatform = mobile::MobilePlatform;
```

4. Add a compile time assertion that `CurrentPlatform` implements `Platform`.

Pros:

- We don't end up working around async trait objects
- We define what functions are needed for a platform
- We save a little on binary size by avoiding a fully generic solution.

Cons:

- The trait does not really do much other than being a collection of functions.
- In some ways it seems like what we're currently doing but with extra steps.
This commit is contained in:
2026-01-24 13:43:40 -07:00
parent 083a11274e
commit ff14f577fe
6 changed files with 459 additions and 22 deletions
+8 -5
View File
@@ -7,7 +7,10 @@ use std::cell::RefCell;
use std::sync::Arc;
use tracing::{error, info};
use crate::imp;
use crate::imp::{CurrentPlatform, PlatformRuntime, SpawnHandleTrait};
/// The spawn handle type for the current platform.
type SpawnHandle = <CurrentPlatform as PlatformRuntime>::SpawnHandle;
static DF_MODEL: Asset = asset!("/assets/DeepFilterNet3_ll_onnx.tar.gz");
// TODO: make this user configurable.
@@ -33,7 +36,7 @@ enum DenoisingModelState {
}
fn with_denoising_model<O>(
spawn: &imp::SpawnHandle,
spawn: &SpawnHandle,
func: impl FnOnce(&mut DfTract) -> O,
) -> Option<O> {
// Using a thread local is super gross, but DfTract is not Send (so it can never leave the current
@@ -89,7 +92,7 @@ fn with_denoising_model<O>(
pub struct AudioProcessor {
denoise: bool,
spawn: imp::SpawnHandle,
spawn: SpawnHandle,
buffer: Vec<f32>,
noise_floor: f32,
/// Whether we were transmitting in the previous frame
@@ -102,7 +105,7 @@ impl AudioProcessor {
pub fn new_plain() -> Self {
AudioProcessor {
denoise: false,
spawn: imp::SpawnHandle::current(),
spawn: SpawnHandle::current(),
buffer: Vec::new(),
noise_floor: DEFAULT_NOISE_FLOOR,
was_transmitting: false,
@@ -113,7 +116,7 @@ impl AudioProcessor {
pub fn new_denoising() -> Self {
AudioProcessor {
denoise: true,
spawn: imp::SpawnHandle::current(),
spawn: SpawnHandle::current(),
buffer: Vec::new(),
noise_floor: DEFAULT_NOISE_FLOOR,
was_transmitting: false,
+105 -4
View File
@@ -1,13 +1,114 @@
use crate::app::Command;
use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver;
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
use mumble_web2_common::ClientConfig;
use mumble_web2_common::{ClientConfig, ServerStatus};
use std::collections::HashMap;
pub use tokio::runtime::Handle as SpawnHandle;
pub use tokio::task::spawn;
pub use tokio::time::sleep;
use std::future::Future;
use std::time::Duration;
use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait};
pub use super::connect::*;
pub use super::native_audio::*;
pub use tokio::task::spawn;
pub use tokio::time::sleep;
// ============================================================================
// Platform Struct
// ============================================================================
/// Desktop platform implementation using Tokio and native audio.
pub struct DesktopPlatform;
// ============================================================================
// SpawnHandle
// ============================================================================
pub type SpawnHandle = tokio::runtime::Handle;
impl SpawnHandleTrait for SpawnHandle {
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let _ = tokio::runtime::Handle::spawn(self, future);
}
fn current() -> Self {
tokio::runtime::Handle::current()
}
}
// ============================================================================
// Trait Implementations
// ============================================================================
impl PlatformRuntime for DesktopPlatform {
type SpawnHandle = SpawnHandle;
fn spawn<F>(future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let _ = tokio::task::spawn(future);
}
async fn sleep(duration: Duration) {
tokio::time::sleep(duration).await;
}
}
impl PlatformConfig for DesktopPlatform {
async fn load_config() -> color_eyre::Result<ClientConfig> {
load_config().await
}
fn load_username() -> Option<String> {
load_username()
}
fn load_server_url() -> Option<String> {
load_server_url()
}
fn set_default_username(username: &str) -> Option<()> {
set_default_username(username)
}
fn set_default_server(server: &str) -> Option<()> {
set_default_server(server)
}
}
impl PlatformNetwork for DesktopPlatform {
async fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> Result<(), Error> {
network_connect(address, username, event_rx, gui_config).await
}
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
get_status(client).await
}
}
impl PlatformInit for DesktopPlatform {
fn init_logging() {
init_logging();
}
fn request_permissions() {
// No-op on desktop
}
}
impl Platform for DesktopPlatform {}
fn get_config_path() -> std::path::PathBuf {
let strategy = choose_app_strategy(AppStrategyArgs {
top_level_domain: "com".to_string(),
+105 -6
View File
@@ -1,16 +1,115 @@
use android_permissions::{PermissionManager, RECORD_AUDIO};
use crate::app::Command;
use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver;
use jni::{objects::JObject, JavaVM};
use mumble_web2_common::ClientConfig;
use mumble_web2_common::{ClientConfig, ServerStatus};
use std::future::Future;
use std::time::Duration;
use std::collections::HashMap;
pub use tokio::runtime::Handle as SpawnHandle;
pub use tokio::task::spawn;
pub use tokio::time::sleep;
use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait};
pub use super::connect::*;
pub use super::native_audio::*;
pub fn set_default_username(username: &str) -> Option<()> {
pub use tokio::task::spawn;
pub use tokio::time::sleep;
// ============================================================================
// Platform Struct
// ============================================================================
/// Mobile platform implementation using Tokio, native audio, and Android permissions.
pub struct MobilePlatform;
// ============================================================================
// SpawnHandle
// ============================================================================
pub type SpawnHandle = tokio::runtime::Handle;
impl SpawnHandleTrait for SpawnHandle {
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let _ = tokio::runtime::Handle::spawn(self, future);
}
fn current() -> Self {
tokio::runtime::Handle::current()
}
}
// ============================================================================
// Trait Implementations
// ============================================================================
impl PlatformRuntime for MobilePlatform {
type SpawnHandle = SpawnHandle;
fn spawn<F>(future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let _ = tokio::task::spawn(future);
}
async fn sleep(duration: Duration) {
tokio::time::sleep(duration).await;
}
}
impl PlatformConfig for MobilePlatform {
async fn load_config() -> color_eyre::Result<ClientConfig> {
load_config().await
}
fn load_username() -> Option<String> {
load_username()
}
fn load_server_url() -> Option<String> {
load_server_url()
}
fn set_default_username(username: &str) -> Option<()> {
set_default_username(username)
}
fn set_default_server(server: &str) -> Option<()> {
set_default_server(server)
}
}
impl PlatformNetwork for MobilePlatform {
async fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> Result<(), Error> {
network_connect(address, username, event_rx, gui_config).await
}
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
get_status(client).await
}
}
impl PlatformInit for MobilePlatform {
fn init_logging() {
init_logging();
}
fn request_permissions() {
request_recording_permission();
}
}
impl Platform for MobilePlatform {}
pub fn set_default_username(_username: &str) -> Option<()> {
None
}
+147 -3
View File
@@ -1,3 +1,126 @@
//! Platform abstraction layer
//!
//! This module defines traits that each platform (web, desktop, mobile) must implement.
//! The traits make the platform boundary explicit and provide compile-time verification.
use crate::app::Command;
use color_eyre::eyre::Error;
use dioxus::hooks::UnboundedReceiver;
use mumble_web2_common::{ClientConfig, ServerStatus};
use std::future::Future;
use std::time::Duration;
// ============================================================================
// Trait Definitions
// ============================================================================
/// Trait for spawn handles that can be stored and used to spawn tasks later.
#[cfg(feature = "web")]
pub trait SpawnHandleTrait: Clone + 'static {
/// Spawn an async task using this handle.
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 'static;
/// Get a spawn handle for the current context.
fn current() -> Self;
}
/// Trait for spawn handles that can be stored and used to spawn tasks later.
#[cfg(any(feature = "desktop", feature = "mobile"))]
pub trait SpawnHandleTrait: Clone + 'static {
/// Spawn an async task using this handle.
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static;
/// Get a spawn handle for the current context.
fn current() -> Self;
}
/// Runtime primitives: task spawning and async sleep.
#[cfg(feature = "web")]
pub trait PlatformRuntime {
/// The spawn handle type for this platform.
type SpawnHandle: SpawnHandleTrait;
/// Spawn an async task.
fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static;
/// Async sleep for the given duration.
fn sleep(duration: Duration) -> impl Future<Output = ()>;
}
/// Runtime primitives: task spawning and async sleep.
#[cfg(any(feature = "desktop", feature = "mobile"))]
pub trait PlatformRuntime {
/// The spawn handle type for this platform.
type SpawnHandle: SpawnHandleTrait;
/// Spawn an async task.
fn spawn<F>(future: F)
where
F: Future<Output = ()> + Send + 'static;
/// Async sleep for the given duration.
fn sleep(duration: Duration) -> impl Future<Output = ()>;
}
/// Configuration persistence: loading and saving user preferences.
pub trait PlatformConfig {
/// Load the client configuration (proxy URL, cert hash, etc.).
fn load_config() -> impl Future<Output = color_eyre::Result<ClientConfig>>;
/// Load saved username.
fn load_username() -> Option<String>;
/// Load saved server URL.
fn load_server_url() -> Option<String>;
/// Save the default username.
fn set_default_username(username: &str) -> Option<()>;
/// Save the default server URL.
fn set_default_server(server: &str) -> Option<()>;
}
/// Network operations: connecting to servers.
pub trait PlatformNetwork {
/// Establish a connection to the Mumble server and run the network loop.
fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> impl Future<Output = Result<(), Error>>;
/// Get server status (user count, version, etc.).
fn get_status(client: &reqwest::Client)
-> impl Future<Output = color_eyre::Result<ServerStatus>>;
}
/// Platform initialization.
pub trait PlatformInit {
/// Initialize logging for the platform.
fn init_logging();
/// Request runtime permissions (Android audio recording, etc.).
fn request_permissions();
}
/// Combined platform trait.
///
/// This is the main trait that each platform must implement. It combines all
/// platform-specific functionality into a single interface, providing compile-time
/// verification that all platforms implement the required functionality.
pub trait Platform: PlatformRuntime + PlatformConfig + PlatformNetwork + PlatformInit {}
// ============================================================================
// Platform Modules
// ============================================================================
#[cfg(feature = "web")]
mod web;
@@ -11,6 +134,30 @@ mod desktop;
#[cfg(feature = "mobile")]
mod mobile;
// ============================================================================
// Platform Type Alias
// ============================================================================
/// The current platform type, selected at compile time based on features.
#[cfg(feature = "web")]
pub type CurrentPlatform = web::WebPlatform;
#[cfg(feature = "desktop")]
pub type CurrentPlatform = desktop::DesktopPlatform;
#[cfg(feature = "mobile")]
pub type CurrentPlatform = mobile::MobilePlatform;
/// Compile-time assertion that CurrentPlatform implements Platform.
const _: () = {
fn assert_platform<T: Platform>() {}
let _ = assert_platform::<CurrentPlatform>;
};
// ============================================================================
// Platform Re-exports
// ============================================================================
#[cfg(feature = "desktop")]
pub use desktop::*;
#[cfg(feature = "mobile")]
@@ -24,6 +171,3 @@ pub fn request_permissions() {}
#[cfg(all(feature = "web", not(any(feature = "desktop", feature = "mobile"))))]
pub use web::*;
#[cfg(any(feature = "desktop"))]
pub use desktop::*;
+91 -1
View File
@@ -29,7 +29,6 @@ use web_sys::AudioWorkletNode;
use web_sys::EncodedAudioChunk;
use web_sys::EncodedAudioChunkInit;
use web_sys::EncodedAudioChunkType;
use web_sys::MediaStream;
use web_sys::MediaStreamConstraints;
use web_sys::MessageEvent;
use web_sys::WebTransport;
@@ -39,6 +38,8 @@ use web_sys::WorkletOptions;
use web_sys::{console, window};
use web_sys::{AudioContext, AudioDataCopyToOptions};
use super::{Platform, PlatformConfig, PlatformInit, PlatformNetwork, PlatformRuntime, SpawnHandleTrait};
pub use wasm_bindgen_futures::spawn_local as spawn;
pub trait ImpRead: AsyncRead + Unpin + 'static {}
@@ -47,10 +48,98 @@ impl<T: AsyncRead + Unpin + 'static> ImpRead for T {}
pub trait ImpWrite: AsyncWrite + Unpin + 'static {}
impl<T: AsyncWrite + Unpin + 'static> ImpWrite for T {}
// ============================================================================
// Platform Struct
// ============================================================================
/// Web platform implementation using WebTransport and Web Audio API.
pub struct WebPlatform;
pub async fn sleep(d: Duration) {
TimeoutFuture::new(d.as_millis() as u32).await
}
// ============================================================================
// Trait Implementations
// ============================================================================
impl SpawnHandleTrait for SpawnHandle {
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
fn current() -> Self {
SpawnHandle
}
}
impl PlatformRuntime for WebPlatform {
type SpawnHandle = SpawnHandle;
fn spawn<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
async fn sleep(duration: Duration) {
TimeoutFuture::new(duration.as_millis() as u32).await;
}
}
impl PlatformConfig for WebPlatform {
async fn load_config() -> color_eyre::Result<ClientConfig> {
load_config().await
}
fn load_username() -> Option<String> {
load_username()
}
fn load_server_url() -> Option<String> {
load_server_url()
}
fn set_default_username(username: &str) -> Option<()> {
set_default_username(username)
}
fn set_default_server(server: &str) -> Option<()> {
set_default_server(server)
}
}
impl PlatformNetwork for WebPlatform {
async fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
gui_config: &ClientConfig,
) -> Result<(), Error> {
network_connect(address, username, event_rx, gui_config).await
}
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
get_status(client).await
}
}
impl PlatformInit for WebPlatform {
fn init_logging() {
init_logging();
}
fn request_permissions() {
// No-op on web
}
}
impl Platform for WebPlatform {}
trait ResultExt<T> {
fn ey(self) -> Result<T, Error>;
}
@@ -495,6 +584,7 @@ pub fn init_logging() {
info!("logging initiated");
}
#[derive(Clone)]
pub struct SpawnHandle;
impl SpawnHandle {
+3 -3
View File
@@ -27,7 +27,7 @@ use tracing::error;
use tracing::info;
use crate::effects::AudioProcessor;
use crate::imp::AudioSystem;
use crate::imp::{AudioSystem, CurrentPlatform, Platform, PlatformNetwork, PlatformRuntime};
pub mod app;
mod effects;
@@ -47,7 +47,7 @@ pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
*STATE.server.write() = Default::default();
*STATE.status.write() = ConnectionState::Connecting;
if let Err(error) = imp::network_connect(address, username, &mut event_rx, &config).await {
if let Err(error) = CurrentPlatform::network_connect(address, username, &mut event_rx, &config).await {
error!("could not connect {:?}", error);
*STATE.status.write() = ConnectionState::Failed(error.to_string());
} else {
@@ -105,7 +105,7 @@ pub async fn network_loop<R: imp::ImpRead, W: imp::ImpWrite>(
break;
}
imp::sleep(Duration::from_millis(3000)).await;
CurrentPlatform::sleep(Duration::from_millis(3000)).await;
}
});
}