| Crates.io | deckyfx-dioxus-ipc-bridge |
| lib.rs | deckyfx-dioxus-ipc-bridge |
| version | 0.1.0 |
| created_at | 2025-11-03 01:31:03.89626+00 |
| updated_at | 2025-11-03 01:31:03.89626+00 |
| description | HTTP-like IPC bridge for Dioxus desktop/web/mobile apps with bidirectional JS-Rust communication |
| homepage | |
| repository | https://github.com/deckyfx/LearningDioxus |
| max_upload_size | |
| id | 1913769 |
| size | 216,890 |
A powerful HTTP-like IPC bridge for Dioxus applications that enables bidirectional communication between JavaScript/React and Rust across desktop, web, and mobile platforms.
:param) and query stringsAdd to your Cargo.toml:
[dependencies]
dioxus = "0.7.0"
dioxus-ipc-bridge = { path = "../dioxus-ipc-bridge" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Here's a complete working example based on the dxbasic implementation:
use dioxus::prelude::*;
use dioxus_ipc_bridge::prelude::*;
use serde_json::json;
fn main() {
dioxus::launch(app);
}
fn app() -> Element {
// 1. Create IPC bridge with timeout configuration
let bridge = IpcBridge::builder()
.timeout(std::time::Duration::from_secs(30))
.build();
// 2. Set up router with routes
let router = use_signal(|| {
IpcRouter::builder()
// Simple route
.route("POST", "/ping", Box::new(PingHandler))
// Route with path parameter
.route("POST", "/greeting/:name", Box::new(GreetingHandler))
// Route with state management
.route("POST", "/counter/increment", Box::new(CounterHandler))
.route("GET", "/counter/value", Box::new(CounterValueHandler))
.build()
});
// 3. Initialize bridge and start router
use_effect(move || {
// Start the router's eval loop to listen for messages from JS
router.read().start();
// Optional: Emit events from Rust to JavaScript
spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
// Emit heartbeat event to JavaScript
bridge::emit("rust:heartbeat", json!({
"timestamp": chrono::Utc::now().to_rfc3339(),
"message": "Heartbeat from Rust"
}));
}
});
});
// 4. Generate and inject bridge script BEFORE other content
let bridge_script = bridge.generate_script();
rsx! {
// IMPORTANT: Inject bridge script first!
script { dangerous_inner_html: "{bridge_script}" }
// Your app content
div { "Dioxus IPC Bridge Ready!" }
}
}
// ========== Route Handlers ==========
/// Simple ping-pong handler
struct PingHandler;
impl RouteHandler for PingHandler {
fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
let timestamp = chrono::Utc::now().to_rfc3339();
let client_time = req.body
.as_ref()
.and_then(|b| match b {
RequestBody::Json(json) => json.get("timestamp").and_then(|t| t.as_str()),
_ => None
})
.unwrap_or("unknown");
Ok(IpcResponse::ok(json!({
"message": "pong",
"server_timestamp": timestamp,
"client_timestamp": client_time
})))
}
}
/// Handler with path parameter
struct GreetingHandler;
impl RouteHandler for GreetingHandler {
fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
// Extract path parameter
let name = req.path_param("name")
.ok_or_else(|| IpcError::BadRequest("Missing name parameter".to_string()))?;
// Extract query parameter (optional)
let language = req.query_param("lang").map(|s| s.as_str()).unwrap_or("en");
let greeting = match language {
"es" => format!("¡Hola, {}!", name),
"fr" => format!("Bonjour, {}!", name),
_ => format!("Hello, {}!", name),
};
Ok(IpcResponse::ok(json!({
"message": greeting,
"name": name,
"language": language
})))
}
}
/// Handler with global state (simplified - use proper state management in production)
static mut GLOBAL_COUNTER: i32 = 0;
struct CounterHandler;
impl RouteHandler for CounterHandler {
fn handle(&self, _req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
unsafe {
GLOBAL_COUNTER += 1;
// Emit event to all JavaScript listeners
bridge::emit("rust:counter:update", json!({
"count": GLOBAL_COUNTER,
"timestamp": chrono::Utc::now().to_rfc3339()
}));
Ok(IpcResponse::ok(json!({
"count": GLOBAL_COUNTER,
"message": "Counter incremented"
})))
}
}
}
struct CounterValueHandler;
impl RouteHandler for CounterValueHandler {
fn handle(&self, _req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
unsafe {
Ok(IpcResponse::ok(json!({
"count": GLOBAL_COUNTER
})))
}
}
}
// Simple GET/POST request
const response = await window.dioxusBridge.fetch('ipc://ping', {
method: 'POST',
body: { timestamp: new Date().toISOString() }
});
console.log(response.body.message); // "pong"
// Request with path parameter
const response = await window.dioxusBridge.fetch('ipc://greeting/World', {
method: 'POST'
});
console.log(response.body.message); // "Hello, World!"
// Request with query string
const response = await window.dioxusBridge.fetch('ipc://greeting/World?lang=es', {
method: 'POST'
});
console.log(response.body.message); // "¡Hola, World!"
// Listen to events from Rust
const subscription = window.dioxusBridge.IPCBridge.on('rust:heartbeat').subscribe({
next: (data) => {
console.log('Heartbeat:', data.message);
},
error: (err) => console.error('Error:', err)
});
// Cleanup when done
subscription.unsubscribe();
The bridge manages the JavaScript-Rust communication layer:
let bridge = IpcBridge::builder()
.timeout(Duration::from_secs(30)) // Request timeout
.build();
// Generate JavaScript initialization code
let bridge_script = bridge.generate_script();
Important: The bridge script must be injected before any JavaScript code that uses window.dioxusBridge.
Routes IPC requests to appropriate handlers:
let router = IpcRouter::builder()
.route("GET", "/path", Box::new(GetHandler))
.route("POST", "/path", Box::new(PostHandler))
.route("GET", "/users/:id", Box::new(UserHandler)) // Path parameter
.build();
// Start listening for requests
router.start();
Implement the RouteHandler trait to handle requests:
struct MyHandler;
impl RouteHandler for MyHandler {
fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
// Extract path parameters
let id = req.path_param("id")?;
// Extract query parameters
let filter = req.query_param("filter").unwrap_or(&"all".to_string());
// Access request body
if let Some(RequestBody::Json(data)) = &req.body {
let name = data["name"].as_str().unwrap();
}
// Return response
Ok(IpcResponse::ok(json!({
"status": "success",
"data": { "id": id }
})))
}
}
Send events from Rust to JavaScript listeners:
use dioxus_ipc_bridge::bridge;
// Emit event
bridge::emit("event:name", json!({
"data": "value"
}));
// Emit event with channel namespace
bridge::emit("rust:counter:update", json!({
"count": 42
}));
The EnrichedRequest provides convenient accessors:
fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
// Path parameters (from :param in route)
let user_id = req.path_param("id")?;
// Query parameters (from ?key=value)
let page = req.query_param("page")
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(1);
// Headers
let content_type = req.header("Content-Type")?;
// Request body (JSON, URL-encoded, or Multipart)
match &req.body {
Some(RequestBody::Json(json)) => {
let name = json["name"].as_str().unwrap();
}
Some(RequestBody::UrlEncoded(fields)) => {
let email = fields.get("email").unwrap();
}
_ => {}
}
Ok(IpcResponse::ok(json!({ "status": "ok" })))
}
For operations that need immediate response AND broadcast updates:
impl RouteHandler for CounterHandler {
fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
let new_value = increment_counter();
// 1. Emit event to ALL listeners (broadcast)
bridge::emit("counter:updated", json!({
"count": new_value
}));
// 2. Return response to requester
Ok(IpcResponse::ok(json!({
"count": new_value
})))
}
}
JavaScript side:
// Listen for broadcasts (all tabs/windows get this)
window.dioxusBridge.IPCBridge.on('counter:updated').subscribe({
next: (data) => setCounter(data.count)
});
// Make request (gets response + triggers broadcast)
const response = await window.dioxusBridge.fetch('ipc://counter/increment', {
method: 'POST'
});
Handle desktop vs web differences:
/// Get current timestamp (works on both desktop and WASM)
fn get_timestamp() -> String {
#[cfg(not(target_arch = "wasm32"))]
{
chrono::Utc::now().to_rfc3339()
}
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::new_0().to_iso_string().as_string().unwrap_or_default()
}
}
// Use platform-specific sleep
#[cfg(not(target_arch = "wasm32"))]
tokio::time::sleep(Duration::from_secs(2)).await;
#[cfg(target_arch = "wasm32")]
{
use gloo_timers::future::TimeoutFuture;
TimeoutFuture::new(2000).await;
}
impl RouteHandler for MyHandler {
fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
// Bad request (400)
let id = req.path_param("id")
.ok_or_else(|| IpcError::BadRequest("Missing id".to_string()))?;
// Not found (404)
if !user_exists(id) {
return Err(IpcError::NotFound(format!("User {} not found", id)));
}
// Custom error with status code
Err(IpcError::Custom {
status: 403,
message: "Access denied".to_string()
})
}
}
| Platform | Status | Notes |
|---|---|---|
| Desktop (Windows/macOS/Linux) | ✅ Fully Supported | Uses dioxus::document::eval() with webview |
| Web (WASM) | ✅ Fully Supported | Uses js_sys for web APIs |
| Mobile (iOS/Android) | ✅ Fully Supported | Same as desktop with mobile webview |
For web builds, add these to your Cargo.toml:
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }
gloo-timers = { version = "0.3", features = ["futures"] }
struct AssetHandler;
impl RouteHandler for AssetHandler {
fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
let name = req.path_param("name")?;
match name.as_str() {
"image" => {
let asset = asset!("/assets/sample.png");
Ok(IpcResponse::ok(json!({
"name": "sample.png",
"type": "image/png",
"data": asset.to_string()
})))
}
_ => Err(IpcError::NotFound("Asset not found".into()))
}
}
}
struct EchoHandler;
impl RouteHandler for EchoHandler {
fn handle(&self, req: &EnrichedRequest) -> Result<IpcResponse, IpcError> {
let message = req.body
.as_ref()
.and_then(|b| match b {
RequestBody::Json(json) => json.get("message").and_then(|m| m.as_str()),
_ => None
})
.unwrap_or("(empty message)");
Ok(IpcResponse::ok(json!({
"echo": message,
"length": message.len(),
"reversed": message.chars().rev().collect::<String>(),
"uppercase": message.to_uppercase()
})))
}
}
use_effect(move || {
spawn(async move {
let mut count = 0;
loop {
tokio::time::sleep(Duration::from_secs(2)).await;
count += 1;
bridge::emit("rust:heartbeat", json!({
"timestamp": chrono::Utc::now().to_rfc3339(),
"message": format!("Heartbeat #{}", count),
"count": count
}));
}
});
});
If requests timeout:
[Rust] window.dioxusBridge readyrouter.read().start()If JavaScript doesn't receive events:
.on(channel).subscribe({ next: ... })For web builds:
web-sys dependency with required features#[cfg(target_arch = "wasm32")])std::fs, tokio, chrono with web alternativesIpcBridge: Bridge configuration and script generationIpcRouter: Route management and request dispatchingIpcRequest: Incoming request from JavaScriptIpcResponse: Response to JavaScriptEnrichedRequest: Parsed request with convenient accessorsRouteHandler: Trait for implementing handlersIpcError: Error types (BadRequest, NotFound, Custom, etc.)RequestBody: Body variants (Json, UrlEncoded, Multipart)// IpcBridge builder
IpcBridge::builder()
.timeout(Duration::from_secs(30))
.build();
// IpcRouter builder
IpcRouter::builder()
.route(method, path, handler)
.build();
// Success responses
IpcResponse::ok(json!({ "data": "value" }))
IpcResponse::created(json!({ "id": 123 }))
// Error responses
IpcResponse::bad_request("Invalid input")
IpcResponse::not_found("Resource not found")
IpcResponse::custom(403, "Forbidden")
See the dxbasic demo for a complete working example with:
Contributions are welcome! Please feel free to submit a Pull Request.
Licensed under either of:
at your option.