sound: initial commit

This commit is contained in:
Stefano Garzarella 2023-03-30 18:47:15 +02:00
parent a3427f5c34
commit 0a78103b41
7 changed files with 350 additions and 0 deletions

16
Cargo.lock generated
View file

@ -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"

View file

@ -4,5 +4,6 @@ members = [
"crates/gpio",
"crates/i2c",
"crates/rng",
"crates/sound",
"crates/vsock",
]

View file

@ -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

24
crates/sound/Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "vhost-user-sound"
version = "0.1.0"
authors = ["Stefano Garzarella <sgarzare@redhat.com>"]
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"

14
crates/sound/README.md Normal file
View file

@ -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)

103
crates/sound/src/main.rs Normal file
View file

@ -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<SoundArgs> for SoundConfig {
type Error = Error;
fn try_from(cmd_args: SoundArgs) -> Result<Self> {
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");
}
}

View file

@ -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<T> = std::result::Result<T, Error>;
/// 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<Error> 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<u64>,
event_idx: bool,
pub(crate) exit_event: EventFd,
mem: Option<GuestMemoryAtomic<GuestMemoryMmap>>,
}
impl VhostUserSoundBackend {
pub fn new(_config: SoundConfig) -> Result<Self> {
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<VringRwLock, ()> 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<GuestMemoryMmap>) -> IoResult<()> {
self.mem = Some(mem);
Ok(())
}
fn handle_event(
&mut self,
device_event: u16,
evset: EventSet,
_vrings: &[VringRwLock],
_thread_id: usize,
) -> IoResult<bool> {
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<u8> {
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<u64> {
self.queues_per_thread.clone()
}
fn exit_event(&self, _thread_index: usize) -> Option<EventFd> {
self.exit_event.try_clone().ok()
}
}