diff --git a/Cargo.lock b/Cargo.lock index 9f8e2ca..b22025e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -864,6 +864,22 @@ dependencies = [ "vmm-sys-util", ] +[[package]] +name = "vhost-user-sound" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "log", + "serial_test", + "thiserror", + "vhost", + "vhost-user-backend", + "virtio-bindings 0.2.0", + "vm-memory", + "vmm-sys-util", +] + [[package]] name = "vhost-user-vsock" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4d1a538..aee431d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ members = [ "crates/gpio", "crates/i2c", "crates/rng", + "crates/sound", "crates/vsock", ] diff --git a/README.md b/README.md index 06673f7..443bc97 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Here is the list of device backends that we support: - [GPIO](https://github.com/rust-vmm/vhost-device/blob/main/crates/gpio/README.md) - [I2C](https://github.com/rust-vmm/vhost-device/blob/main/crates/i2c/README.md) - [RNG](https://github.com/rust-vmm/vhost-device/blob/main/crates/rng/README.md) +- [Sound](https://github.com/rust-vmm/vhost-device/blob/main/crates/sound/README.md) - [VSOCK](https://github.com/rust-vmm/vhost-device/blob/main/crates/vsock/README.md) ## Testing and Code Coverage diff --git a/crates/sound/Cargo.toml b/crates/sound/Cargo.toml new file mode 100644 index 0000000..c387a36 --- /dev/null +++ b/crates/sound/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "vhost-user-sound" +version = "0.1.0" +authors = ["Stefano Garzarella "] +description = "A virtio-sound device using the vhost-user protocol." +repository = "https://github.com/rust-vmm/vhost-device" +readme = "README.md" +keywords = ["vhost", "sound", "virtio-sound"] +license = "Apache-2.0 OR BSD-3-Clause" +edition = "2018" + +[dependencies] +clap = { version = "4.1", features = ["derive"] } +env_logger = "0.10" +log = "0.4" +thiserror = "1.0" +vhost = { version = "0.6", features = ["vhost-user-slave"] } +vhost-user-backend = "0.8" +virtio-bindings = "0.2" +vm-memory = "0.10" +vmm-sys-util = "0.11" + +[dev-dependencies] +serial_test = "1.0" diff --git a/crates/sound/README.md b/crates/sound/README.md new file mode 100644 index 0000000..ea2fbce --- /dev/null +++ b/crates/sound/README.md @@ -0,0 +1,14 @@ +# vhost-user-sound + +## Design + +## Usage + +## Working example + +## License + +This project is licensed under either of + +- [Apache License](http://www.apache.org/licenses/LICENSE-2.0), Version 2.0 +- [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause) diff --git a/crates/sound/src/main.rs b/crates/sound/src/main.rs new file mode 100644 index 0000000..8fb8c43 --- /dev/null +++ b/crates/sound/src/main.rs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +mod vhu_sound; + +use std::{ + convert::TryFrom, + sync::{Arc, RwLock}, +}; + +use clap::Parser; +use log::{info, warn}; +use vhost::{vhost_user, vhost_user::Listener}; +use vhost_user_backend::VhostUserDaemon; +use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; + +use crate::vhu_sound::{Error, Result, SoundConfig, VhostUserSoundBackend}; + +#[derive(Parser, Debug)] +#[clap(version, about, long_about = None)] +struct SoundArgs { + /// vhost-user Unix domain socket path. + #[clap(long)] + socket: String, +} + +impl TryFrom for SoundConfig { + type Error = Error; + + fn try_from(cmd_args: SoundArgs) -> Result { + let socket = cmd_args.socket.trim().to_string(); + + Ok(SoundConfig::new(socket)) + } +} + +/// This is the public API through which an external program starts the +/// vhost-user-sound backend server. +pub(crate) fn start_backend_server(config: SoundConfig) { + loop { + let backend = Arc::new(RwLock::new( + VhostUserSoundBackend::new(config.clone()).unwrap(), + )); + + let listener = Listener::new(config.get_socket_path(), true).unwrap(); + + let mut daemon = VhostUserDaemon::new( + String::from("vhost-user-sound"), + backend.clone(), + GuestMemoryAtomic::new(GuestMemoryMmap::new()), + ) + .unwrap(); + + daemon.start(listener).unwrap(); + + match daemon.wait() { + Ok(()) => { + info!("Stopping cleanly"); + } + Err(vhost_user_backend::Error::HandleRequest(vhost_user::Error::PartialMessage)) => { + info!("vhost-user connection closed with partial message. If the VM is shutting down, this is expected behavior; otherwise, it might be a bug."); + } + Err(e) => { + warn!("Error running daemon: {:?}", e); + } + } + + // No matter the result, we need to shut down the worker thread. + backend.read().unwrap().exit_event.write(1).unwrap(); + } +} + +fn main() { + env_logger::init(); + + let config = SoundConfig::try_from(SoundArgs::parse()).unwrap(); + start_backend_server(config); +} + +#[cfg(test)] +mod tests { + use super::*; + use serial_test::serial; + + impl SoundArgs { + fn from_args(socket: &str) -> Self { + SoundArgs { + socket: socket.to_string(), + } + } + } + + #[test] + #[serial] + fn test_vsock_config_setup() { + let args = SoundArgs::from_args("/tmp/vhost-sound.socket"); + + let config = SoundConfig::try_from(args); + assert!(config.is_ok()); + + let config = config.unwrap(); + assert_eq!(config.get_socket_path(), "/tmp/vhost-sound.socket"); + } +} diff --git a/crates/sound/src/vhu_sound.rs b/crates/sound/src/vhu_sound.rs new file mode 100644 index 0000000..77d6ae4 --- /dev/null +++ b/crates/sound/src/vhu_sound.rs @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +use std::{ + io::{self, Result as IoResult}, + u16, u32, u64, u8, +}; +use thiserror::Error as ThisError; +use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures}; +use vhost_user_backend::{VhostUserBackendMut, VringRwLock}; +use virtio_bindings::bindings::{ + virtio_config::VIRTIO_F_NOTIFY_ON_EMPTY, virtio_config::VIRTIO_F_VERSION_1, + virtio_ring::VIRTIO_RING_F_EVENT_IDX, virtio_ring::VIRTIO_RING_F_INDIRECT_DESC, +}; +use vm_memory::{ByteValued, GuestMemoryAtomic, GuestMemoryMmap, Le32}; +use vmm_sys_util::{ + epoll::EventSet, + eventfd::{EventFd, EFD_NONBLOCK}, +}; + +const CONTROL_Q: u16 = 0; +const EVENT_Q: u16 = 1; +const TX_Q: u16 = 2; +const RX_Q: u16 = 3; + +pub(crate) type Result = std::result::Result; + +/// Custom error types +#[derive(Debug, ThisError)] +pub(crate) enum Error { + #[error("Failed to handle event other than EPOLLIN event")] + HandleEventNotEpollIn, + #[error("Failed to handle unknown event")] + HandleUnknownEvent, + #[error("Failed to create a new EventFd")] + EventFdCreate(std::io::Error), +} + +impl std::convert::From for std::io::Error { + fn from(e: Error) -> Self { + std::io::Error::new(io::ErrorKind::Other, e) + } +} + +#[derive(Debug, Clone)] +/// This structure is the public API through which an external program +/// is allowed to configure the backend. +pub(crate) struct SoundConfig { + socket: String, +} + +impl SoundConfig { + /// Create a new instance of the SoundConfig struct, containing the + /// parameters to be fed into the sound-backend server. + pub fn new(socket: String) -> Self { + Self { socket } + } + + /// Return the path of the unix domain socket which is listening to + /// requests from the guest. + pub fn get_socket_path(&self) -> String { + String::from(&self.socket) + } +} + +/// Virtio Sound Configuration +#[derive(Copy, Clone, Debug, Default, PartialEq)] +#[repr(C)] +struct VirtioSoundConfig { + /// total number of all available jacks + jacks: Le32, + /// total number of all available PCM streams + streams: Le32, + /// total number of all available channel maps + chmpas: Le32, +} +// SAFETY: The layout of the structure is fixed and can be initialized by +// reading its content from byte array. +unsafe impl ByteValued for VirtioSoundConfig {} + +/// Virtio Sound Request / Response common header +#[derive(Copy, Clone, Debug, Default, PartialEq)] +#[repr(C)] +struct VirtioSoundHeader { + /// request type / response status + code: Le32, +} +// SAFETY: The layout of the structure is fixed and can be initialized by +// reading its content from byte array. +unsafe impl ByteValued for VirtioSoundHeader {} + +pub(crate) struct VhostUserSoundBackend { + config: VirtioSoundConfig, + queues_per_thread: Vec, + event_idx: bool, + pub(crate) exit_event: EventFd, + mem: Option>, +} + +impl VhostUserSoundBackend { + pub fn new(_config: SoundConfig) -> Result { + let queues_per_thread = vec![0b1111]; + + Ok(Self { + config: VirtioSoundConfig { + jacks: 0.into(), + streams: 1.into(), + chmpas: 0.into(), + }, + queues_per_thread, + event_idx: false, + exit_event: EventFd::new(EFD_NONBLOCK).map_err(Error::EventFdCreate)?, + mem: None, + }) + } +} + +impl VhostUserBackendMut for VhostUserSoundBackend { + fn num_queues(&self) -> usize { + 4 + } + + fn max_queue_size(&self) -> usize { + 256 + } + + fn features(&self) -> u64 { + 1 << VIRTIO_F_VERSION_1 + | 1 << VIRTIO_F_NOTIFY_ON_EMPTY + | 1 << VIRTIO_RING_F_INDIRECT_DESC + | 1 << VIRTIO_RING_F_EVENT_IDX + | VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits() + } + + fn protocol_features(&self) -> VhostUserProtocolFeatures { + VhostUserProtocolFeatures::CONFIG + } + + fn set_event_idx(&mut self, enabled: bool) { + self.event_idx = enabled; + } + + fn update_memory(&mut self, mem: GuestMemoryAtomic) -> IoResult<()> { + self.mem = Some(mem); + Ok(()) + } + + fn handle_event( + &mut self, + device_event: u16, + evset: EventSet, + _vrings: &[VringRwLock], + _thread_id: usize, + ) -> IoResult { + if evset != EventSet::IN { + return Err(Error::HandleEventNotEpollIn.into()); + } + + match device_event { + CONTROL_Q => {} + EVENT_Q => {} + TX_Q => {} + RX_Q => {} + _ => { + return Err(Error::HandleUnknownEvent.into()); + } + } + + Ok(false) + } + + fn get_config(&self, offset: u32, size: u32) -> Vec { + let offset = offset as usize; + let size = size as usize; + + let buf = self.config.as_slice(); + + if offset + size > buf.len() { + return Vec::new(); + } + + buf[offset..offset + size].to_vec() + } + + fn queues_per_thread(&self) -> Vec { + self.queues_per_thread.clone() + } + + fn exit_event(&self, _thread_index: usize) -> Option { + self.exit_event.try_clone().ok() + } +}