Skip to content

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 ViiperClient or async AsyncViiperClient (with async feature)
  • Type-safe: Generated structs with constants, helper maps, and DeviceInput trait implementations
  • Callback-based output: Register closures for device feedback (LEDs, rumble)
  • Zero external dependencies (sync): Uses only std for the synchronous client
  • Tokio-based async: Optional async feature 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

Install the client library using Cargo:

cargo add viiper-client

For async support:

cargo add viiper-client --features async
cargo add tokio --features full

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:

[dependencies]
viiper-client = { path = "../../clients/rust" }

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:

stream.on_output(|reader| {
    let mut buf = [0u8; 2];
    reader.read_exact(&mut buf)?;
    let left_motor = buf[0];
    let right_motor = buf[1];
    println!("Rumble: Left={} Right={}", left_motor, right_motor);
    Ok(())
}).expect("Failed to register callback");
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:

stream.on_output(|reader| async move {
    let mut buf = [0u8; 2];
    reader.read_exact(&mut buf)?;
    let left_motor = buf[0];
    let right_motor = buf[1];
    println!("Rumble: Left={} Right={}", left_motor, right_motor);
    Ok(())
}).expect("Failed to register callback");

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:

use viiper_client::devices::keyboard::*;

let mods = MOD_LEFT_SHIFT | MOD_LEFT_CTRL;  // 0x03

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:

[dependencies]
viiper-client = { version = "0.1", features = ["async"] }

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