Rust Client Library Documentation
The VIIPER Rust client library provides a type-safe, zero-cost abstraction client library for interacting with VIIPER servers and controlling virtual devices.
The Rust client library features:
- Sync and Async APIs: Choose between blocking
ViiperClientor asyncAsyncViiperClient(withasyncfeature) - Type-safe: Generated structs with constants, helper maps, and
DeviceInputtrait implementations - Callback-based output: Register closures for device feedback (LEDs, rumble)
- Zero external dependencies (sync): Uses only
stdfor the synchronous client - Tokio-based async: Optional
asyncfeature for async/await support with Tokio runtime
License
The Rust client library is licensed under the MIT License, providing maximum flexibility for integration into your projects.
The core VIIPER server remains under its original license.
Installation
1. Using the Published Crate (Recommended)
Install the client library using Cargo:
For async support:
Package page: viiper-client on crates.io
Pre-release / snapshot builds are not published to crates.io. They are only available as GitHub Release artifacts (e.g.
dev-latest) or by building from source.
2. Path Dependency (For Local Development Against Source)
Use this when modifying the generator or contributing new device types:
Example
use viiper_client::{ViiperClient, devices::keyboard::*};
use std::net::ToSocketAddrs;
fn main() {
// Create new Viiper client
let addr = "localhost:3242"
.to_socket_addrs()
.expect("Invalid address")
.next()
.expect("No address resolved");
let client = ViiperClient::new(addr);
// Find or create a bus
let bus_id = match client.bus_list() {
Ok(resp) if resp.buses.is_empty() => {
client.bus_create(None).expect("Failed to create bus").bus_id
}
Ok(resp) => *resp.buses.first().unwrap(),
Err(e) => panic!("BusList error: {}", e),
};
// Add device
let device_info = client.bus_device_add(
bus_id,
&viiper_client::types::DeviceCreateRequest {
r#type: Some("keyboard".to_string()),
id_vendor: None,
id_product: None,
device_specific: None,
},
).expect("Failed to add device");
// Connect to device stream
let mut stream = client
.connect_device(device_info.bus_id, &device_info.dev_id)
.expect("Failed to connect");
println!("Connected to device {} on bus {}", device_info.dev_id, device_info.bus_id);
// Send keyboard input
let input = KeyboardInput {
modifiers: MOD_LEFT_SHIFT,
count: 1,
keys: vec![KEY_H],
};
stream.send(&input).expect("Failed to send input");
// Cleanup
let _ = client.bus_device_remove(device_info.bus_id, Some(&device_info.dev_id));
}
use tokio::time::{sleep, Duration};
use viiper_client::{AsyncViiperClient, devices::keyboard::*};
use std::net::ToSocketAddrs;
#[tokio::main]
async fn main() {
// Create new Viiper client
let addr = "localhost:3242"
.to_socket_addrs()
.expect("Invalid address")
.next()
.expect("No address resolved");
let client = AsyncViiperClient::new(addr);
// Find or create a bus
let bus_id = match client.bus_list().await {
Ok(resp) if resp.buses.is_empty() => {
client.bus_create(None).await.expect("Failed to create bus").bus_id
}
Ok(resp) => *resp.buses.first().unwrap(),
Err(e) => panic!("BusList error: {}", e),
};
// Add device
let device_info = client.bus_device_add(
bus_id,
&viiper_client::types::DeviceCreateRequest {
r#type: Some("keyboard".to_string()),
id_vendor: None,
id_product: None,
device_specific: None,
},
).await.expect("Failed to add device");
// Connect to device stream
let mut stream = client
.connect_device(device_info.bus_id, &device_info.dev_id)
.await
.expect("Failed to connect");
println!("Connected to device {} on bus {}", device_info.dev_id, device_info.bus_id);
// Send keyboard input
let input = KeyboardInput {
modifiers: MOD_LEFT_SHIFT,
count: 1,
keys: vec![KEY_H],
};
stream.send(&input).await.expect("Failed to send input");
// Cleanup
let _ = client.bus_device_remove(device_info.bus_id, Some(&device_info.dev_id)).await;
}
Device Control/Feedback
Creating a Device + Control/Feedback Stream
use viiper_client::{ViiperClient, types::DeviceCreateRequest};
use std::net::ToSocketAddrs;
let addr = "localhost:3242"
.to_socket_addrs()
.expect("Invalid address")
.next()
.expect("No address resolved");
let client = ViiperClient::new(addr);
// Add device first
let device_info = client.bus_device_add(
bus_id,
&DeviceCreateRequest {
r#type: Some("xbox360".to_string()),
id_vendor: None,
id_product: None,
},
).expect("Failed to add device");
// Then connect to its stream
let mut stream = client
.connect_device(device_info.bus_id, &device_info.dev_id)
.expect("Failed to connect");
Sending Input
Device input is sent using generated structs that implement the DeviceInput trait:
use viiper_client::devices::xbox360::*;
let input = Xbox360Input {
buttons: BUTTON_A as u32,
lt: 255,
rt: 0,
lx: -32768, // Left stick left
ly: 32767, // Left stick up
rx: 0,
ry: 0,
};
stream.send(&input).expect("Failed to send");
Receiving Feedback
For devices that send feedback (rumble, LEDs), register a callback with on_output:
use viiper_client::devices::keyboard::OUTPUT_SIZE;
stream.on_output(|reader| {
let mut buf = [0u8; OUTPUT_SIZE];
reader.read_exact(&mut buf)?;
let leds = buf[0];
let num_lock = (leds & 0x01) != 0;
let caps_lock = (leds & 0x02) != 0;
let scroll_lock = (leds & 0x04) != 0;
println!("LEDs: Num={} Caps={} Scroll={}", num_lock, caps_lock, scroll_lock);
Ok(())
}).expect("Failed to register callback");
For Xbox360 rumble:
use tokio::io::AsyncReadExt;
use viiper_client::devices::keyboard::OUTPUT_SIZE;
stream.on_output(|stream| async move {
let mut buf = [0u8; OUTPUT_SIZE];
let mut guard = stream.lock().await;
guard.read_exact(&mut buf).await?;
drop(guard);
let leds = buf[0];
let num_lock = (leds & 0x01) != 0;
let caps_lock = (leds & 0x02) != 0;
println!("LEDs: Num={} Caps={}", num_lock, caps_lock);
Ok(())
}).expect("Failed to register callback");
For Xbox360 rumble:
Generated Constants and Maps
The Rust client library generates constants and lazy-static maps for each device type.
Keyboard Constants
Key Constants:
use viiper_client::devices::keyboard::*;
let key = KEY_A; // 0x04
let f1 = KEY_F1; // 0x3A
let enter = KEY_ENTER; // 0x28
Modifier Flags:
LED Flags:
use viiper_client::devices::keyboard::*;
let num_lock = (leds & LED_NUM_LOCK) != 0;
let caps_lock = (leds & LED_CAPS_LOCK) != 0;
Helper Maps
The client library generates useful lookup maps for working with keyboard input:
CHAR_TO_KEY - Convert ASCII characters to key codes:
use viiper_client::devices::keyboard::CHAR_TO_KEY;
if let Some(&key) = CHAR_TO_KEY.get(&b'a') {
println!("'a' maps to key code {}", key); // KEY_A
}
KEY_NAME - Get human-readable key names:
use viiper_client::devices::keyboard::KEY_NAME;
if let Some(name) = KEY_NAME.get(&KEY_F1) {
println!("Key name: {}", name); // "F1"
}
SHIFT_CHARS - Check if a character requires shift:
use viiper_client::devices::keyboard::SHIFT_CHARS;
let needs_shift = SHIFT_CHARS.contains(&b'A'); // true for uppercase
Error Handling
The client library uses a custom ViiperError type for all errors:
use viiper_client::ViiperError;
match client.bus_list() {
Ok(buses) => println!("Found {} buses", buses.buses.len()),
Err(ViiperError::Io(e)) => eprintln!("I/O error: {}", e),
Err(ViiperError::Protocol(problem)) => eprintln!("API error: {}", problem),
Err(e) => eprintln!("Other error: {}", e),
}
The server returns errors as RFC 7807 Problem inspired JSON.
The client parses these into ProblemJson:
use viiper_client::ProblemJson;
if let Err(ViiperError::Protocol(problem)) = result {
println!("Status: {}", problem.status);
println!("Title: {}", problem.title);
println!("Detail: {}", problem.detail);
}
Features
The Rust client library supports optional features:
| Feature | Description | Dependencies |
|---|---|---|
| (default) | Synchronous blocking client | None |
async |
Async client with Tokio runtime | tokio, tokio-util |
Enable async support:
Examples
Full working examples are available in the repository:
-
Virtual Keyboard (sync):
examples/rust/sync/virtual_keyboard/- Types "Hello!" every 5 seconds using generated maps
- Displays LED feedback in console
-
Virtual Keyboard (async):
examples/rust/async/virtual_keyboard/- Async version using Tokio runtime
-
Virtual Mouse (sync/async):
examples/rust/sync/virtual_mouse/,examples/rust/async/virtual_mouse/- Moves cursor diagonally
- Demonstrates button clicks and scroll wheel
-
Virtual Xbox360 Controller (sync/async):
examples/rust/sync/virtual_x360_pad/,examples/rust/async/virtual_x360_pad/- Cycles through buttons
- Handles rumble feedback
See Also
- Generator Documentation: How generated client libraries work
- Go Client Documentation: Reference implementation patterns
- C# Client Library Documentation: Alternative managed language client library
- TypeScript Client Library Documentation: Node.js client library
- C++ Client Library Documentation: Header-only C++ client library
- API Overview: Management API reference
- Device Documentation: Wire formats and device-specific details