├── qapi ├── COPYING ├── src │ ├── futures │ │ ├── tower.rs │ │ ├── codec.rs │ │ ├── tokio.rs │ │ └── mod.rs │ └── lib.rs └── Cargo.toml ├── qga ├── COPYING ├── schema │ └── qga │ │ └── qapi-schema.json ├── build.rs ├── Cargo.toml └── src │ └── lib.rs ├── qmp ├── COPYING ├── schema │ └── qapi │ │ ├── cxl.json │ │ ├── job.json │ │ ├── net.json │ │ ├── pci.json │ │ ├── qom.json │ │ ├── tpm.json │ │ ├── ui.json │ │ ├── acpi.json │ │ ├── audio.json │ │ ├── authz.json │ │ ├── block.json │ │ ├── char.json │ │ ├── dump.json │ │ ├── ebpf.json │ │ ├── error.json │ │ ├── misc.json │ │ ├── qdev.json │ │ ├── stats.json │ │ ├── trace.json │ │ ├── uefi.json │ │ ├── vfio.json │ │ ├── yank.json │ │ ├── common.json │ │ ├── compat.json │ │ ├── control.json │ │ ├── crypto.json │ │ ├── machine.json │ │ ├── pragma.json │ │ ├── replay.json │ │ ├── rocker.json │ │ ├── sockets.json │ │ ├── virtio.json │ │ ├── block-core.json │ │ ├── cryptodev.json │ │ ├── introspect.json │ │ ├── migration.json │ │ ├── run-state.json │ │ ├── block-export.json │ │ ├── misc-target.json │ │ ├── qapi-schema.json │ │ ├── transaction.json │ │ ├── machine-common.json │ │ └── machine-target.json ├── build.rs ├── Cargo.toml └── src │ └── lib.rs ├── spec ├── COPYING ├── Cargo.toml └── src │ └── lib.rs ├── codegen ├── COPYING ├── Cargo.toml └── src │ └── lib.rs ├── parser ├── COPYING ├── Cargo.toml └── src │ └── lib.rs ├── .gitignore ├── lock.nix ├── .gitmodules ├── .vim └── coc-settings.json ├── Cargo.toml ├── examples ├── Cargo.toml └── src │ └── bin │ ├── tokio_qmp_events.rs │ ├── guest_info.rs │ ├── tokio_qmp_query.rs │ ├── tokio_guest_info.rs │ └── qmp_query.rs ├── default.nix ├── filter-schema.sh ├── clone-schema.sh ├── COPYING ├── ci.nix ├── README.md ├── flake.lock ├── .github └── workflows │ └── qapi-rs.yml ├── flake.nix └── Cargo.lock /qapi/COPYING: -------------------------------------------------------------------------------- 1 | ../COPYING -------------------------------------------------------------------------------- /qga/COPYING: -------------------------------------------------------------------------------- 1 | ../COPYING -------------------------------------------------------------------------------- /qmp/COPYING: -------------------------------------------------------------------------------- 1 | ../COPYING -------------------------------------------------------------------------------- /spec/COPYING: -------------------------------------------------------------------------------- 1 | ../COPYING -------------------------------------------------------------------------------- /codegen/COPYING: -------------------------------------------------------------------------------- 1 | ../COPYING -------------------------------------------------------------------------------- /parser/COPYING: -------------------------------------------------------------------------------- 1 | ../COPYING -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /result* 3 | /.cargo/ 4 | -------------------------------------------------------------------------------- /qmp/schema/qapi/cxl.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/cxl.json -------------------------------------------------------------------------------- /qmp/schema/qapi/job.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/job.json -------------------------------------------------------------------------------- /qmp/schema/qapi/net.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/net.json -------------------------------------------------------------------------------- /qmp/schema/qapi/pci.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/pci.json -------------------------------------------------------------------------------- /qmp/schema/qapi/qom.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/qom.json -------------------------------------------------------------------------------- /qmp/schema/qapi/tpm.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/tpm.json -------------------------------------------------------------------------------- /qmp/schema/qapi/ui.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/ui.json -------------------------------------------------------------------------------- /qmp/schema/qapi/acpi.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/acpi.json -------------------------------------------------------------------------------- /qmp/schema/qapi/audio.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/audio.json -------------------------------------------------------------------------------- /qmp/schema/qapi/authz.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/authz.json -------------------------------------------------------------------------------- /qmp/schema/qapi/block.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/block.json -------------------------------------------------------------------------------- /qmp/schema/qapi/char.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/char.json -------------------------------------------------------------------------------- /qmp/schema/qapi/dump.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/dump.json -------------------------------------------------------------------------------- /qmp/schema/qapi/ebpf.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/ebpf.json -------------------------------------------------------------------------------- /qmp/schema/qapi/error.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/error.json -------------------------------------------------------------------------------- /qmp/schema/qapi/misc.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/misc.json -------------------------------------------------------------------------------- /qmp/schema/qapi/qdev.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/qdev.json -------------------------------------------------------------------------------- /qmp/schema/qapi/stats.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/stats.json -------------------------------------------------------------------------------- /qmp/schema/qapi/trace.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/trace.json -------------------------------------------------------------------------------- /qmp/schema/qapi/uefi.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/uefi.json -------------------------------------------------------------------------------- /qmp/schema/qapi/vfio.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/vfio.json -------------------------------------------------------------------------------- /qmp/schema/qapi/yank.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/yank.json -------------------------------------------------------------------------------- /lock.nix: -------------------------------------------------------------------------------- 1 | { 2 | outputHashes = { 3 | 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /qmp/schema/qapi/common.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/common.json -------------------------------------------------------------------------------- /qmp/schema/qapi/compat.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/compat.json -------------------------------------------------------------------------------- /qmp/schema/qapi/control.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/control.json -------------------------------------------------------------------------------- /qmp/schema/qapi/crypto.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/crypto.json -------------------------------------------------------------------------------- /qmp/schema/qapi/machine.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/machine.json -------------------------------------------------------------------------------- /qmp/schema/qapi/pragma.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/pragma.json -------------------------------------------------------------------------------- /qmp/schema/qapi/replay.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/replay.json -------------------------------------------------------------------------------- /qmp/schema/qapi/rocker.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/rocker.json -------------------------------------------------------------------------------- /qmp/schema/qapi/sockets.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/sockets.json -------------------------------------------------------------------------------- /qmp/schema/qapi/virtio.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/virtio.json -------------------------------------------------------------------------------- /qga/schema/qga/qapi-schema.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qga/qapi-schema.json -------------------------------------------------------------------------------- /qmp/schema/qapi/block-core.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/block-core.json -------------------------------------------------------------------------------- /qmp/schema/qapi/cryptodev.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/cryptodev.json -------------------------------------------------------------------------------- /qmp/schema/qapi/introspect.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/introspect.json -------------------------------------------------------------------------------- /qmp/schema/qapi/migration.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/migration.json -------------------------------------------------------------------------------- /qmp/schema/qapi/run-state.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/run-state.json -------------------------------------------------------------------------------- /qmp/schema/qapi/block-export.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/block-export.json -------------------------------------------------------------------------------- /qmp/schema/qapi/misc-target.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/misc-target.json -------------------------------------------------------------------------------- /qmp/schema/qapi/qapi-schema.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/qapi-schema.json -------------------------------------------------------------------------------- /qmp/schema/qapi/transaction.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/transaction.json -------------------------------------------------------------------------------- /qmp/schema/qapi/machine-common.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/machine-common.json -------------------------------------------------------------------------------- /qmp/schema/qapi/machine-target.json: -------------------------------------------------------------------------------- 1 | ../../../schema/qapi/machine-target.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "schema"] 2 | path = schema 3 | url = https://github.com/arcnmx/qemu-qapi-filtered.git 4 | -------------------------------------------------------------------------------- /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.cargo.features": [ 3 | "qapi-qmp", "qapi-qga", "tower-service" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "parser", 5 | "codegen", 6 | "spec", 7 | "qmp", 8 | "qga", 9 | "qapi", 10 | "examples", 11 | ] 12 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qapi-examples" 3 | version = "0.0.0" 4 | authors = ["arcnmx"] 5 | edition = "2018" 6 | publish = false 7 | 8 | [dependencies] 9 | qapi = { version = "0.15", path = "../qapi", features = ["qmp", "qga", "async-tokio-all"] } 10 | tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread"] } 11 | futures = "0.3" 12 | env_logger = "0.10" 13 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | lockData = builtins.fromJSON (builtins.readFile ./flake.lock); 3 | sourceInfo = lockData.nodes.std.locked; 4 | src = fetchTarball { 5 | url = "https://github.com/${sourceInfo.owner}/${sourceInfo.repo}/archive/${sourceInfo.rev}.tar.gz"; 6 | sha256 = sourceInfo.narHash; 7 | }; 8 | in (import src).Flake.Bootstrap { 9 | path = ./.; 10 | inherit lockData; 11 | loadWith.defaultPackage = null; 12 | } 13 | -------------------------------------------------------------------------------- /filter-schema.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ $# -ne 1 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | TAG="$1" 10 | 11 | git switch --create="$TAG-filtered" "$TAG" 12 | git filter-repo \ 13 | --path-regex '^qapi-schema\.json$|^qapi/[^/]*\.json$|^qga/[^/]*\.json$|^VERSION$' \ 14 | --prune-empty always \ 15 | --force # --force flag tells `git filter-repo` to proceed even though it doesn't consider this a "fresh clone" 16 | -------------------------------------------------------------------------------- /codegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qapi-codegen" 3 | version = "0.11.3" # keep in sync with html_root_url 4 | authors = ["arcnmx"] 5 | edition = "2018" 6 | 7 | description = "QEMU QAPI codegen helper" 8 | keywords = ["qemu", "qmp", "qga", "qapi"] 9 | 10 | documentation = "https://docs.rs/qapi-codegen/" 11 | repository = "https://github.com/arcnmx/qapi-rs" 12 | readme = "../README.md" 13 | license = "MIT" 14 | 15 | [badges] 16 | travis-ci = { repository = "arcnmx/qapi-rs" } 17 | maintenance = { status = "passively-maintained" } 18 | 19 | [dependencies] 20 | qapi-parser = { version = "0.11", path = "../parser" } 21 | -------------------------------------------------------------------------------- /parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qapi-parser" 3 | version = "0.11.0" # keep in sync with html_root_url 4 | authors = ["arcnmx"] 5 | edition = "2018" 6 | 7 | description = "QEMU QAPI JSON specification parser" 8 | keywords = ["qemu", "qmp", "qga", "qapi"] 9 | 10 | documentation = "https://docs.rs/qapi-parser/" 11 | repository = "https://github.com/arcnmx/qapi-rs" 12 | readme = "../README.md" 13 | license = "MIT" 14 | 15 | [badges] 16 | travis-ci = { repository = "arcnmx/qapi-rs" } 17 | maintenance = { status = "passively-maintained" } 18 | 19 | [dependencies] 20 | serde = { version = "^1.0.27", features = [ "derive" ] } 21 | serde_json = "^1.0.9" 22 | -------------------------------------------------------------------------------- /spec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qapi-spec" 3 | version = "0.3.2" # keep in sync with html_root_url 4 | authors = ["arcnmx"] 5 | edition = "2018" 6 | 7 | description = "QEMU QAPI common types" 8 | keywords = ["qemu", "qmp", "qga", "qapi"] 9 | 10 | documentation = "https://docs.rs/qapi-spec/" 11 | repository = "https://github.com/arcnmx/qapi-rs" 12 | readme = "../README.md" 13 | license = "MIT" 14 | 15 | [badges] 16 | travis-ci = { repository = "arcnmx/qapi-rs" } 17 | maintenance = { status = "passively-maintained" } 18 | 19 | [dependencies] 20 | serde = { version = "^1.0.27", features = [ "derive" ] } 21 | serde_json = "^1.0.9" 22 | base64 = "^0.22.0" 23 | -------------------------------------------------------------------------------- /clone-schema.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | if [ $# -ne 1 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | TAG="$1" 10 | OLD_PWD=$PWD 11 | 12 | git clone https://github.com/qemu/qemu.git /tmp/qemu 13 | cd /tmp/qemu 14 | 15 | if ! git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then 16 | echo "Error: Tag '$TAG' does not exist in the repository" 17 | exit 1 18 | fi 19 | 20 | $OLD_PWD/filter-schema.sh "$TAG" 21 | git remote add filtered git@github.com:arcnmx/qemu-qapi-filtered.git 22 | git push filtered --tags --quiet 23 | # the branch "$TAG-filtered" is created by filter-schema.sh 24 | git push filtered "$TAG-filtered" -------------------------------------------------------------------------------- /qga/build.rs: -------------------------------------------------------------------------------- 1 | extern crate qapi_codegen; 2 | 3 | use std::{io, env, path}; 4 | 5 | fn main() { 6 | match main_result() { 7 | Ok(()) => (), 8 | Err(e) => panic!("{:?}", e), 9 | } 10 | } 11 | 12 | fn main_result() -> io::Result<()> { 13 | println!("rerun-if-changed=build.rs"); 14 | 15 | let out_dir = path::Path::new(&env::var("OUT_DIR").unwrap()).join("qga.rs"); 16 | let schema_dir = path::Path::new(&env::var_os("CARGO_MANIFEST_DIR").unwrap()) 17 | .join("schema") 18 | .join("qga"); 19 | 20 | for inc in qapi_codegen::codegen(schema_dir, out_dir, "QgaCommand".into())? { 21 | println!("rerun-if-changed={}", inc.display()); 22 | } 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /qmp/build.rs: -------------------------------------------------------------------------------- 1 | extern crate qapi_codegen; 2 | 3 | use std::{io, env, path}; 4 | 5 | fn main() { 6 | match main_result() { 7 | Ok(()) => (), 8 | Err(e) => panic!("{:?}", e), 9 | } 10 | } 11 | 12 | fn main_result() -> io::Result<()> { 13 | println!("rerun-if-changed=build.rs"); 14 | 15 | let out_dir = path::Path::new(&env::var("OUT_DIR").unwrap()).join("qmp.rs"); 16 | let schema_dir = path::Path::new(&env::var_os("CARGO_MANIFEST_DIR").unwrap()) 17 | .join("schema") 18 | .join("qapi"); 19 | 20 | for inc in qapi_codegen::codegen(schema_dir, out_dir, "QmpCommand".into())? { 21 | println!("rerun-if-changed={}", inc.display()); 22 | } 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /qmp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qapi-qmp" 3 | version = "0.15.0" # keep in sync with html_root_url 4 | build = "build.rs" 5 | authors = ["arcnmx"] 6 | edition = "2018" 7 | 8 | description = "QEMU Machine Protocol types" 9 | keywords = ["qemu", "qmp", "qapi"] 10 | 11 | documentation = "https://docs.rs/qapi-qmp/" 12 | repository = "https://github.com/arcnmx/qapi-rs" 13 | readme = "../README.md" 14 | license = "MIT" 15 | 16 | [badges] 17 | travis-ci = { repository = "arcnmx/qapi-rs" } 18 | maintenance = { status = "passively-maintained" } 19 | 20 | [build-dependencies] 21 | qapi-codegen = { version = "0.11.1", path = "../codegen" } 22 | 23 | [dependencies] 24 | serde = { version = "^1.0.27", features = [ "derive" ] } 25 | qapi-spec = { version = "0.3", path = "../spec" } 26 | -------------------------------------------------------------------------------- /qga/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qapi-qga" 3 | version = "0.13.0" # keep in sync with html_root_url 4 | build = "build.rs" 5 | authors = ["arcnmx"] 6 | edition = "2018" 7 | 8 | description = "QEMU Guest Agent protocol types" 9 | keywords = ["qemu", "qga", "qapi"] 10 | 11 | documentation = "https://docs.rs/qapi-qga/" 12 | repository = "https://github.com/arcnmx/qapi-rs" 13 | readme = "../README.md" 14 | license = "MIT" 15 | 16 | [badges] 17 | travis-ci = { repository = "arcnmx/qapi-rs" } 18 | maintenance = { status = "passively-maintained" } 19 | 20 | [build-dependencies] 21 | qapi-codegen = { version = "0.11.3", path = "../codegen" } 22 | 23 | [dependencies] 24 | serde = { version = "^1.0.27", features = [ "derive" ] } 25 | qapi-spec = { version = "0.3", path = "../spec" } 26 | -------------------------------------------------------------------------------- /examples/src/bin/tokio_qmp_events.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | use std::io; 3 | use futures::StreamExt; 4 | 5 | #[tokio::main] 6 | async fn main() -> io::Result<()> { 7 | ::env_logger::init(); 8 | 9 | let socket_addr = args().nth(1).expect("argument: QMP socket path"); 10 | 11 | #[cfg(unix)] 12 | let stream = qapi::futures::QmpStreamTokio::open_uds(socket_addr).await?; 13 | #[cfg(not(unix))] 14 | let stream = qapi::futures::QmpStreamTokio::open_tcp(socket_addr).await?; 15 | println!("{:#?}", stream.capabilities); 16 | let stream = stream.negotiate().await?; 17 | let (_, mut events) = stream.into_parts(); 18 | 19 | while let Some(event) = events.next().await { 20 | println!("Got event {:#?}", event?); 21 | } 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /examples/src/bin/guest_info.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use std::os::unix::net::UnixStream; 3 | use std::env::args; 4 | use qapi::{qga, Qga}; 5 | 6 | pub fn main() { 7 | ::env_logger::init(); 8 | 9 | let socket_addr = args().nth(1).expect("argument: QEMU Guest Agent socket path"); 10 | #[cfg(unix)] 11 | let stream = UnixStream::connect(socket_addr).expect("failed to connect to socket"); 12 | #[cfg(not(unix))] 13 | let stream = std::net::TcpStream::connect(socket_addr).expect("failed to connect to socket"); 14 | 15 | let mut qga = Qga::from_stream(&stream); 16 | 17 | let sync_value = &stream as *const _ as usize as i32; 18 | qga.guest_sync(sync_value).expect("handshake failed"); 19 | 20 | let info = qga.execute(&qga::guest_info { }).unwrap(); 21 | println!("Guest Agent version: {}", info.version); 22 | } 23 | -------------------------------------------------------------------------------- /qapi/src/futures/tower.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | use futures::{Sink, Future, FutureExt}; 5 | use tower_service::Service; 6 | use crate::{Command, Execute, ExecuteError}; 7 | use super::QapiService; 8 | 9 | // this really doesn't work well for lifetime reasons? 10 | 11 | impl Service for QapiService where 12 | W: Sink, Error=io::Error> + Unpin + Send, 13 | { 14 | type Response = C::Ok; 15 | type Error = ExecuteError; 16 | type Future = Pin> + 'static>>; 17 | 18 | fn poll_ready(&mut self, _: &mut Context) -> Poll> { 19 | Poll::Ready(Ok(())) 20 | } 21 | 22 | fn call(&mut self, req: C) -> Self::Future { 23 | self.execute(req).boxed() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/src/bin/tokio_qmp_query.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | use std::io; 3 | 4 | #[tokio::main] 5 | pub async fn main() -> io::Result<()> { 6 | ::env_logger::init(); 7 | 8 | let socket_addr = args().nth(1).expect("argument: QMP socket path"); 9 | 10 | #[cfg(unix)] 11 | let stream = qapi::futures::QmpStreamTokio::open_uds(socket_addr).await?; 12 | #[cfg(not(unix))] 13 | let stream = qapi::futures::QmpStreamTokio::open_tcp(socket_addr).await?; 14 | println!("{:#?}", stream.capabilities); 15 | let stream = stream.negotiate().await?; 16 | let (qmp, handle) = stream.spawn_tokio(); 17 | 18 | let status = qmp.execute(qapi::qmp::query_status { }).await?; 19 | println!("VCPU status: {:#?}", status); 20 | 21 | { 22 | // NOTE: this isn't necessary, but to manually ensure the stream closes... 23 | drop(qmp); // relinquish handle on the stream 24 | handle.await?; // wait for event loop to exit 25 | } 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /examples/src/bin/tokio_guest_info.rs: -------------------------------------------------------------------------------- 1 | use std::env::args; 2 | use std::io; 3 | 4 | #[tokio::main] 5 | async fn main() -> io::Result<()> { 6 | ::env_logger::init(); 7 | 8 | let socket_addr = args().nth(1).expect("argument: QEMU Guest Agent socket path"); 9 | 10 | #[cfg(unix)] 11 | let stream = qapi::futures::QgaStreamTokio::open_uds(socket_addr).await?; 12 | #[cfg(not(unix))] 13 | let stream = qapi::futures::QgaStreamTokio::open_tcp(socket_addr).await?; 14 | let (qga, handle) = stream.spawn_tokio(); 15 | 16 | let sync_value = &qga as *const _ as usize as i32; 17 | qga.guest_sync(sync_value).await?; 18 | 19 | let info = qga.execute(qapi::qga::guest_info { }).await?; 20 | println!("Guest Agent version: {}", info.version); 21 | 22 | { 23 | // NOTE: this isn't necessary, but to manually ensure the stream closes... 24 | drop(qga); // relinquish handle on the stream 25 | handle.await?; // wait for event loop to exit 26 | } 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /examples/src/bin/qmp_query.rs: -------------------------------------------------------------------------------- 1 | use std::thread::sleep; 2 | use std::time::Duration; 3 | use std::env::args; 4 | #[cfg(unix)] 5 | use std::os::unix::net::UnixStream; 6 | use qapi::{qmp, Qmp}; 7 | 8 | pub fn main() { 9 | ::env_logger::init(); 10 | 11 | let socket_addr = args().nth(1).expect("argument: QMP socket path"); 12 | #[cfg(unix)] 13 | let stream = UnixStream::connect(socket_addr).expect("failed to connect to socket"); 14 | #[cfg(not(unix))] 15 | let stream = std::net::TcpStream::connect(socket_addr).expect("failed to connect to socket"); 16 | 17 | let mut qmp = Qmp::from_stream(&stream); 18 | 19 | let info = qmp.handshake().expect("handshake failed"); 20 | println!("QMP info: {:#?}", info); 21 | 22 | let status = qmp.execute(&qmp::query_status { }).unwrap(); 23 | println!("VCPU status: {:#?}", status); 24 | 25 | loop { 26 | qmp.nop().unwrap(); 27 | for event in qmp.events() { 28 | println!("Got event: {:#?}", event); 29 | } 30 | 31 | sleep(Duration::from_secs(1)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 arcnmx 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /ci.nix: -------------------------------------------------------------------------------- 1 | { pkgs, env, lib, ... }: with pkgs; with lib; let 2 | inherit (import ./. { pkgs = null; }) checks packages; 3 | in { 4 | config = { 5 | name = "qapi-rs"; 6 | ci.version = "v0.7"; 7 | ci.gh-actions.enable = true; 8 | cache.cachix = { 9 | ci.signingKey = ""; 10 | arc.enable = true; 11 | }; 12 | channels = { 13 | nixpkgs = mkIf (env.platform != "impure") "24.05"; 14 | }; 15 | tasks = with checks; { 16 | test.inputs = [ 17 | test-qapi test-qapi-all 18 | test-qapi-qmp test-qapi-qga 19 | test-qapi-async test-qapi-tokio 20 | ]; 21 | parser.inputs = [ checks.test-parser ]; 22 | spec.inputs = [ checks.test-spec ]; 23 | codegen.inputs = [ checks.test-codegen ]; 24 | qga.inputs = [ checks.test-qga ]; 25 | qmp.inputs = [ checks.test-qmp ]; 26 | examples.inputs = [ packages.examples ]; 27 | }; 28 | jobs = { 29 | nixos = { 30 | tasks = { 31 | windows.inputs = [ packages.examples-windows ]; 32 | }; 33 | }; 34 | macos.system = "aarch64-darwin"; 35 | }; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qapi-rs 2 | 3 | [![release-badge][]][cargo] [![docs-badge][]][docs] [![license-badge][]][license] 4 | 5 | A rust library for interfacing with [QEMU](https://www.qemu.org/) QAPI sockets. 6 | 7 | ## [Documentation][docs] 8 | 9 | See the [documentation][docs] for up to date information, as well as the 10 | reference documentation for both the [QEMU Machine Protocol](https://qemu-project.gitlab.io/qemu/interop/qemu-qmp-ref.html) 11 | and [Guest Agent](https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html) APIs. 12 | 13 | There are two features (`qga` and `qmp`) which enable their respective functionality. 14 | They can be enabled in your `Cargo.toml`: 15 | 16 | ```toml 17 | [dependencies] 18 | qapi = { version = "0.15", features = [ "qmp" ] } 19 | ``` 20 | 21 | ### Examples 22 | 23 | Short examples are available for both [QMP](examples/src/bin/qmp_query.rs) and [Guest 24 | Agent](examples/src/bin/guest_info.rs). Async/nonblocking examples using tokio [are also 25 | available](examples/src/bin/tokio_qmp_query.rs). 26 | 27 | [release-badge]: https://img.shields.io/crates/v/qapi.svg?style=flat-square 28 | [cargo]: https://crates.io/crates/qapi 29 | [docs-badge]: https://img.shields.io/badge/API-docs-blue.svg?style=flat-square 30 | [docs]: https://docs.rs/qapi/ 31 | [license-badge]: https://img.shields.io/badge/license-MIT-ff69b4.svg?style=flat-square 32 | [license]: https://github.com/arcnmx/qapi-rs/blob/master/COPYING 33 | -------------------------------------------------------------------------------- /qapi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qapi" 3 | version = "0.15.0" # keep in sync with README and html_root_url 4 | authors = ["arcnmx"] 5 | edition = "2018" 6 | 7 | description = "QEMU QMP and Guest Agent API" 8 | keywords = ["qemu", "qmp", "qga", "qapi"] 9 | 10 | documentation = "https://docs.rs/qapi" 11 | repository = "https://github.com/arcnmx/qapi-rs" 12 | readme = "../README.md" 13 | license = "MIT" 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | 18 | [badges] 19 | travis-ci = { repository = "arcnmx/qapi-rs" } 20 | maintenance = { status = "passively-maintained" } 21 | 22 | [dependencies] 23 | log = "^0.4.6" 24 | serde = "^1.0.27" 25 | serde_json = "^1.0.9" 26 | 27 | tokio = { version = "^1.0.0", default-features = false, features = ["io-util"], optional = true } 28 | tower-service = { version = "^0.3.0", optional = true } 29 | tokio-util = { version = "^0.7.0", features = ["codec"], optional = true } 30 | futures = { version = "^0.3.5", optional = true } 31 | memchr = { version = "^2.3.3", optional = true } 32 | bytes = { version = "^1.0.0", optional = true } 33 | 34 | qapi-spec = { version = "0.3", path = "../spec" } 35 | qapi-qga = { version = "0.13", path = "../qga", optional = true } 36 | qapi-qmp = { version = "0.15", path = "../qmp", optional = true } 37 | 38 | [features] 39 | qga = ["qapi-qga"] 40 | qmp = ["qapi-qmp"] 41 | async = ["futures"] 42 | async-tokio = ["async", "tokio", "tokio-util", "bytes", "memchr"] 43 | async-tokio-net = ["async-tokio", "tokio/net"] 44 | async-tokio-spawn = ["async-tokio", "tokio/rt"] 45 | async-tokio-all = ["async-tokio-net", "async-tokio-spawn"] 46 | async-tower = ["async", "tower-service"] 47 | -------------------------------------------------------------------------------- /qga/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case, non_camel_case_types)] 2 | #![doc(html_root_url = "https://docs.rs/qapi-qga/0.13.0")] 3 | 4 | include!(concat!(env!("OUT_DIR"), "/qga.rs")); 5 | 6 | use std::{io, str, fmt, error}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | pub trait QgaCommand: qapi_spec::Command { } 10 | impl<'a, T: QgaCommand> QgaCommand for &'a T { } 11 | impl<'a, T: QgaCommand> QgaCommand for &'a mut T { } 12 | 13 | #[derive(Copy, Clone, Debug, Deserialize, Serialize)] 14 | #[serde(rename_all = "kebab-case")] 15 | pub enum GuestShutdownMode { 16 | Halt, 17 | Powerdown, 18 | Reboot, 19 | } 20 | 21 | impl GuestExecStatus { 22 | pub fn result(self) -> Result { 23 | if self.exited { 24 | if self.exitcode != Some(0) || self.signal.is_some() { 25 | Err(self) 26 | } else { 27 | Ok(self) 28 | } 29 | } else { 30 | Ok(self) 31 | } 32 | } 33 | 34 | fn message(&self) -> String { 35 | let (err0, err1) = if let Some(Ok(data)) = self.err_data.as_ref().map(|s| str::from_utf8(s)) { 36 | (": ", data) 37 | } else { 38 | ("", "") 39 | }; 40 | 41 | let sig = if let Some(signal) = self.signal { 42 | format!(" (terminated by signal {})", signal) 43 | } else { 44 | Default::default() 45 | }; 46 | 47 | if let Some(code) = self.exitcode { 48 | format!("guest process exited with code {}{}{}{}", code, sig, err0, err1) 49 | } else if self.exited { 50 | format!("guest process exited{}{}{}", sig, err0, err1) 51 | } else { 52 | format!("guest process is still running") 53 | } 54 | } 55 | } 56 | 57 | impl fmt::Display for GuestExecStatus { 58 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 59 | write!(f, "{}", self.message()) 60 | } 61 | } 62 | 63 | impl error::Error for GuestExecStatus { 64 | fn description(&self) -> &str { 65 | "guest process exit status" 66 | } 67 | } 68 | 69 | impl From for io::Error { 70 | fn from(s: GuestExecStatus) -> Self { 71 | io::Error::new(io::ErrorKind::Other, s.to_string()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /qapi/src/futures/codec.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::marker::PhantomData; 3 | use bytes::{BytesMut, BufMut}; 4 | use serde::{de::DeserializeOwned, Serialize}; 5 | 6 | pub struct JsonLinesCodec { 7 | next_index: usize, 8 | _decoder: PhantomData D>, 9 | } 10 | 11 | impl JsonLinesCodec { 12 | pub fn new() -> Self { 13 | Self { 14 | next_index: 0, 15 | _decoder: PhantomData, 16 | } 17 | } 18 | } 19 | 20 | impl JsonLinesCodec { 21 | fn priv_decode(&mut self, buf: &mut BytesMut) -> Result, io::Error> { 22 | match memchr::memchr(b'\n', &buf[self.next_index..]) { 23 | Some(offset) => { 24 | let index = offset + self.next_index; 25 | self.next_index = 0; 26 | let line = buf.split_to(index + 1); 27 | serde_json::from_slice(&line) 28 | .map_err(From::from) 29 | .map(Some) 30 | }, 31 | None => { 32 | self.next_index = buf.len(); 33 | Ok(None) 34 | }, 35 | } 36 | } 37 | 38 | fn priv_decode_eof(&mut self, buf: &mut BytesMut) -> Result, io::Error> { 39 | if buf.is_empty() { 40 | Ok(None) 41 | } else { 42 | serde_json::from_slice(buf) 43 | .map_err(From::from) 44 | .map(Some) 45 | } 46 | } 47 | } 48 | 49 | #[cfg(feature = "tokio-util")] 50 | impl tokio_util::codec::Decoder for JsonLinesCodec { 51 | type Item = D; 52 | type Error = io::Error; 53 | 54 | fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { 55 | self.priv_decode(buf) 56 | } 57 | 58 | fn decode_eof(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { 59 | self.priv_decode_eof(buf) 60 | } 61 | } 62 | 63 | fn encode(item: S, bytes: &mut BytesMut) -> Result<(), io::Error> { 64 | serde_json::to_writer(bytes.writer(), &item)?; 65 | bytes.put_u8(b'\n'); 66 | Ok(()) 67 | } 68 | 69 | #[cfg(feature = "tokio-util")] 70 | impl tokio_util::codec::Encoder for JsonLinesCodec { 71 | type Error = io::Error; 72 | 73 | fn encode(&mut self, item: S, bytes: &mut BytesMut) -> Result<(), Self::Error> { 74 | encode(item, bytes) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /qmp/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case, non_camel_case_types)] 2 | #![doc(html_root_url = "https://docs.rs/qapi-qmp/0.15.0")] 3 | #![allow(deprecated)] 4 | 5 | use std::io; 6 | use std::string::String as StdString; 7 | use std::convert::TryFrom; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | include!(concat!(env!("OUT_DIR"), "/qmp.rs")); 11 | 12 | pub type QmpMessageAny = QmpMessage; 13 | 14 | pub trait QmpCommand: qapi_spec::Command { } 15 | impl<'a, T: QmpCommand> QmpCommand for &'a T { } 16 | impl<'a, T: QmpCommand> QmpCommand for &'a mut T { } 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize)] 19 | #[serde(untagged)] 20 | pub enum QmpMessage { 21 | Event(Event), 22 | Response(qapi_spec::Response), 23 | } 24 | 25 | impl TryFrom> for qapi_spec::Response { 26 | type Error = io::Error; 27 | 28 | fn try_from(m: QmpMessage) -> Result { 29 | match m { 30 | QmpMessage::Response(res) => Ok(res), 31 | QmpMessage::Event(..) => 32 | Err(io::Error::new(io::ErrorKind::InvalidData, "QMP event where a response was expected")), 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone, Serialize, Deserialize)] 38 | pub struct QMP { 39 | pub version: VersionInfo, 40 | pub capabilities: Vec, 41 | } 42 | 43 | #[derive(Debug, Clone, Serialize, Deserialize)] 44 | #[serde(untagged)] 45 | pub enum QmpCapability { 46 | #[serde(rename = "oob")] 47 | OutOfBand, 48 | Unknown(qapi_spec::Any), 49 | } 50 | 51 | #[derive(Debug, Clone, Serialize, Deserialize)] 52 | pub struct QapiCapabilities { 53 | pub QMP: QMP, 54 | } 55 | 56 | impl QapiCapabilities { 57 | pub fn supports_oob(&self) -> bool { 58 | self.QMP.capabilities.iter().any(|c| match c { 59 | QmpCapability::OutOfBand => true, 60 | _ => false, 61 | }) 62 | } 63 | 64 | pub fn capabilities<'a>(&'a self) -> impl Iterator + 'a { 65 | self.QMP.capabilities.iter().filter_map(|c| match c { 66 | QmpCapability::OutOfBand => Some(QMPCapability::oob), 67 | QmpCapability::Unknown(..) => None, 68 | }) 69 | } 70 | } 71 | 72 | impl device_add { 73 | pub fn new, I: Into>, B: Into>, P: IntoIterator>(driver: D, id: I, bus: B, props: P) -> Self { 74 | device_add { 75 | driver: driver.into(), 76 | id: id.into(), 77 | bus: bus.into(), 78 | arguments: props.into_iter().collect(), 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fl-config": { 4 | "locked": { 5 | "lastModified": 1653159448, 6 | "narHash": "sha256-PvB9ha0r4w6p412MBPP71kS/ZTBnOjxL0brlmyucPBA=", 7 | "owner": "flakelib", 8 | "repo": "fl", 9 | "rev": "fcefb9738d5995308a24cda018a083ccb6b0f460", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "flakelib", 14 | "ref": "config", 15 | "repo": "fl", 16 | "type": "github" 17 | } 18 | }, 19 | "flakelib": { 20 | "inputs": { 21 | "fl-config": "fl-config", 22 | "std": "std" 23 | }, 24 | "locked": { 25 | "lastModified": 1701802971, 26 | "narHash": "sha256-Zo5fJpXbe+xXOTiDT4JG2rExobMJTmFZ72+3XTMMHrQ=", 27 | "owner": "flakelib", 28 | "repo": "fl", 29 | "rev": "b71a91517f6b16aa5faefe8ec491d9f3062d7a20", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "flakelib", 34 | "repo": "fl", 35 | "type": "github" 36 | } 37 | }, 38 | "nix-std": { 39 | "locked": { 40 | "lastModified": 1701658249, 41 | "narHash": "sha256-KIt1TUuBvldhaVRta010MI5FeQlB8WadjqljybjesN0=", 42 | "owner": "chessai", 43 | "repo": "nix-std", 44 | "rev": "715db541ffff4194620e48d210b76f73a74b5b5d", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "chessai", 49 | "repo": "nix-std", 50 | "type": "github" 51 | } 52 | }, 53 | "nixpkgs": { 54 | "locked": { 55 | "lastModified": 1725857262, 56 | "narHash": "sha256-m9n0PncgZepVgmjOO1rfVXMgUACDOwZbhjSRjJ/NUpM=", 57 | "owner": "NixOS", 58 | "repo": "nixpkgs", 59 | "rev": "5af6aefbcc55670e36663fd1f8a796e1e323001a", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "id": "nixpkgs", 64 | "type": "indirect" 65 | } 66 | }, 67 | "root": { 68 | "inputs": { 69 | "flakelib": "flakelib", 70 | "nixpkgs": "nixpkgs", 71 | "rust": "rust", 72 | "schema": "schema" 73 | } 74 | }, 75 | "rust": { 76 | "inputs": { 77 | "nixpkgs": [ 78 | "nixpkgs" 79 | ] 80 | }, 81 | "locked": { 82 | "lastModified": 1732847979, 83 | "narHash": "sha256-037fkyB/HAZjl35geOt9fnE+JB3UvTIPYiQyfFz8/jw=", 84 | "owner": "arcnmx", 85 | "repo": "nixexprs-rust", 86 | "rev": "1bb6c5eb824224efe05447582652c872ddc1cee3", 87 | "type": "github" 88 | }, 89 | "original": { 90 | "owner": "arcnmx", 91 | "repo": "nixexprs-rust", 92 | "type": "github" 93 | } 94 | }, 95 | "schema": { 96 | "flake": false, 97 | "locked": { 98 | "lastModified": 1753206370, 99 | "narHash": "sha256-ZUezhnA6/yYDUxZkCy651rCQJ8j4+msQgLXOVu7wB7Y=", 100 | "owner": "arcnmx", 101 | "repo": "qemu-qapi-filtered", 102 | "rev": "e7805e8c7ba1e0c6da1b3cce48a12eea122a8e61", 103 | "type": "github" 104 | }, 105 | "original": { 106 | "owner": "arcnmx", 107 | "ref": "v10.0.3", 108 | "repo": "qemu-qapi-filtered", 109 | "type": "github" 110 | } 111 | }, 112 | "std": { 113 | "inputs": { 114 | "nix-std": "nix-std" 115 | }, 116 | "locked": { 117 | "lastModified": 1701802337, 118 | "narHash": "sha256-JCVCyjDZ6LA0xyVoDZzRXjy0OgWOZo3OpeZEVm/U97w=", 119 | "owner": "flakelib", 120 | "repo": "std", 121 | "rev": "443d1c8246b3d96a4822b02af907ca0d833e8b63", 122 | "type": "github" 123 | }, 124 | "original": { 125 | "owner": "flakelib", 126 | "repo": "std", 127 | "type": "github" 128 | } 129 | } 130 | }, 131 | "root": "root", 132 | "version": 7 133 | } 134 | -------------------------------------------------------------------------------- /.github/workflows/qapi-rs.yml: -------------------------------------------------------------------------------- 1 | env: 2 | CI_ALLOW_ROOT: '1' 3 | CI_CONFIG: ./ci.nix 4 | CI_PLATFORM: gh-actions 5 | jobs: 6 | ci-check: 7 | name: qapi-rs check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - id: checkout 11 | name: git clone 12 | uses: actions/checkout@v4 13 | with: 14 | submodules: true 15 | - id: nix-install 16 | name: nix install 17 | uses: arcnmx/ci/actions/nix/install@v0.7 18 | - id: ci-action-build 19 | name: nix build ci.gh-actions.configFile 20 | uses: arcnmx/ci/actions/nix/build@v0.7 21 | with: 22 | attrs: ci.gh-actions.configFile 23 | out-link: .ci/workflow.yml 24 | - id: ci-action-compare 25 | name: gh-actions compare 26 | uses: arcnmx/ci/actions/nix/run@v0.7 27 | with: 28 | args: -u .github/workflows/qapi-rs.yml .ci/workflow.yml 29 | attrs: nixpkgs.diffutils 30 | command: diff 31 | macos: 32 | name: qapi-rs-macos 33 | runs-on: macos-latest 34 | steps: 35 | - id: checkout 36 | name: git clone 37 | uses: actions/checkout@v4 38 | with: 39 | submodules: true 40 | - id: nix-install 41 | name: nix install 42 | uses: arcnmx/ci/actions/nix/install@v0.7 43 | - id: ci-setup 44 | name: nix setup 45 | uses: arcnmx/ci/actions/nix/run@v0.7 46 | with: 47 | attrs: ci.job.macos.run.setup 48 | quiet: false 49 | - id: ci-dirty 50 | name: nix test dirty 51 | uses: arcnmx/ci/actions/nix/run@v0.7 52 | with: 53 | attrs: ci.job.macos.run.test 54 | command: ci-build-dirty 55 | quiet: false 56 | stdout: ${{ runner.temp }}/ci.build.dirty 57 | - id: ci-test 58 | name: nix test build 59 | uses: arcnmx/ci/actions/nix/run@v0.7 60 | with: 61 | attrs: ci.job.macos.run.test 62 | command: ci-build-realise 63 | ignore-exit-code: true 64 | quiet: false 65 | stdin: ${{ runner.temp }}/ci.build.dirty 66 | - env: 67 | CI_EXIT_CODE: ${{ steps.ci-test.outputs.exit-code }} 68 | id: ci-summary 69 | name: nix test results 70 | uses: arcnmx/ci/actions/nix/run@v0.7 71 | with: 72 | attrs: ci.job.macos.run.test 73 | command: ci-build-summarise 74 | quiet: false 75 | stdin: ${{ runner.temp }}/ci.build.dirty 76 | stdout: ${{ runner.temp }}/ci.build.cache 77 | - env: 78 | CACHIX_SIGNING_KEY: ${{ secrets.CACHIX_SIGNING_KEY }} 79 | id: ci-cache 80 | if: always() 81 | name: nix test cache 82 | uses: arcnmx/ci/actions/nix/run@v0.7 83 | with: 84 | attrs: ci.job.macos.run.test 85 | command: ci-build-cache 86 | quiet: false 87 | stdin: ${{ runner.temp }}/ci.build.cache 88 | nixos: 89 | name: qapi-rs-nixos 90 | runs-on: ubuntu-latest 91 | steps: 92 | - id: checkout 93 | name: git clone 94 | uses: actions/checkout@v4 95 | with: 96 | submodules: true 97 | - id: nix-install 98 | name: nix install 99 | uses: arcnmx/ci/actions/nix/install@v0.7 100 | - id: ci-setup 101 | name: nix setup 102 | uses: arcnmx/ci/actions/nix/run@v0.7 103 | with: 104 | attrs: ci.job.nixos.run.setup 105 | quiet: false 106 | - id: ci-dirty 107 | name: nix test dirty 108 | uses: arcnmx/ci/actions/nix/run@v0.7 109 | with: 110 | attrs: ci.job.nixos.run.test 111 | command: ci-build-dirty 112 | quiet: false 113 | stdout: ${{ runner.temp }}/ci.build.dirty 114 | - id: ci-test 115 | name: nix test build 116 | uses: arcnmx/ci/actions/nix/run@v0.7 117 | with: 118 | attrs: ci.job.nixos.run.test 119 | command: ci-build-realise 120 | ignore-exit-code: true 121 | quiet: false 122 | stdin: ${{ runner.temp }}/ci.build.dirty 123 | - env: 124 | CI_EXIT_CODE: ${{ steps.ci-test.outputs.exit-code }} 125 | id: ci-summary 126 | name: nix test results 127 | uses: arcnmx/ci/actions/nix/run@v0.7 128 | with: 129 | attrs: ci.job.nixos.run.test 130 | command: ci-build-summarise 131 | quiet: false 132 | stdin: ${{ runner.temp }}/ci.build.dirty 133 | stdout: ${{ runner.temp }}/ci.build.cache 134 | - env: 135 | CACHIX_SIGNING_KEY: ${{ secrets.CACHIX_SIGNING_KEY }} 136 | id: ci-cache 137 | if: always() 138 | name: nix test cache 139 | uses: arcnmx/ci/actions/nix/run@v0.7 140 | with: 141 | attrs: ci.job.nixos.run.test 142 | command: ci-build-cache 143 | quiet: false 144 | stdin: ${{ runner.temp }}/ci.build.cache 145 | name: qapi-rs 146 | 'on': 147 | - push 148 | - pull_request 149 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "QEMU QMP and Guest Agent API"; 3 | inputs = { 4 | flakelib.url = "github:flakelib/fl"; 5 | nixpkgs = { }; 6 | schema = { 7 | flake = false; 8 | type = "github"; 9 | owner = "arcnmx"; 10 | repo = "qemu-qapi-filtered"; 11 | ref = "v10.0.3"; # keep in sync with schema submodule 12 | }; 13 | rust = { 14 | url = "github:arcnmx/nixexprs-rust"; 15 | inputs.nixpkgs.follows = "nixpkgs"; 16 | }; 17 | }; 18 | outputs = { self, schema, flakelib, nixpkgs, rust, ... }@inputs: let 19 | featureMatrix = rec { 20 | qmp = [ "qmp" ]; 21 | qga = [ "qga" ]; 22 | all = qmp ++ qga; 23 | async = all ++ [ "async-tower" ]; 24 | tokio = all ++ [ "async-tokio-all" ]; 25 | }; 26 | nixlib = nixpkgs.lib; 27 | inherit (self.lib) crate; 28 | libs = nixlib.filterAttrs (_: crate: crate.package.publish or true) crate.members; 29 | testCrate = package: { buildFeatures ? [ ], ... }@args: with nixlib; let 30 | crate = self.lib.crate.members.${package}; 31 | flags = [ "-p" crate.name ]; 32 | in { rustPlatform, source }: rustPlatform.buildRustPackage (args // { 33 | pname = crate.name; 34 | inherit (crate) cargoLock version; 35 | src = source; 36 | cargoTestFlags = flags ++ args.cargoTestFlags or [ ]; 37 | cargoBuildFlags = flags ++ args.cargoBuildFlags or [ ]; 38 | buildType = "debug"; 39 | meta = { 40 | name = let 41 | features = " --features ${concatStringsSep "," buildFeatures}"; 42 | cmd = if args.doCheck or true then "test" else "build"; 43 | in "cargo ${cmd} -p ${crate.name}" + optionalString (buildFeatures != [ ]) features; 44 | } // args.meta or { }; 45 | auditable = false; 46 | passthru.ci = { 47 | cache.inputs = [ (rustPlatform.importCargoLock crate.cargoLock) ]; 48 | }; 49 | }); 50 | in flakelib { 51 | inherit inputs; 52 | systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 53 | devShells = { 54 | plain = { 55 | mkShell, writeShellScriptBin 56 | , enableRust ? true, cargo 57 | , rustTools ? [ ] 58 | , generate 59 | }: mkShell { 60 | inherit rustTools; 61 | nativeBuildInputs = nixlib.optional enableRust cargo 62 | ++ [ 63 | (writeShellScriptBin "generate" ''nix run .#generate "$@"'') 64 | ]; 65 | }; 66 | stable = { rust'stable, outputs'devShells'plain }: outputs'devShells'plain.override { 67 | inherit (rust'stable) mkShell; 68 | enableRust = false; 69 | }; 70 | dev = { rust'unstable, rust-w64-overlay, outputs'devShells'plain }: let 71 | channel = rust'unstable.override { 72 | channelOverlays = [ rust-w64-overlay ]; 73 | }; 74 | in outputs'devShells'plain.override { 75 | inherit (channel) mkShell; 76 | enableRust = false; 77 | rustTools = [ "rust-analyzer" ]; 78 | }; 79 | default = { outputs'devShells }: outputs'devShells.plain; 80 | }; 81 | packages = { 82 | examples = testCrate "examples" { 83 | doCheck = false; 84 | }; 85 | examples-windows = { rust-w64, examples }: (examples.override { 86 | inherit (rust-w64.latest) rustPlatform; 87 | }).overrideAttrs (old: { 88 | meta = old.meta // { 89 | name = "cargo build --target ${rust-w64.latest.hostTarget.triple} -p qapi-examples"; 90 | }; 91 | }); 92 | default = { examples }: examples; 93 | }; 94 | legacyPackages = { 95 | # manual src fixup for submodule symlinks 96 | source = { runCommand }: runCommand crate.src.name { 97 | preferLocalBuild = true; 98 | inherit (crate) src; 99 | inherit (crate.src) pname version; 100 | inherit schema; 101 | readme = ./README.md; 102 | } '' 103 | mkdir $out 104 | cp --no-preserve=mode -rs $src/* $out/ 105 | for api in qmp qga; do 106 | cp -frd $src/$api/schema/* $out/$api/schema/ 107 | done 108 | ln -s $schema $out/schema 109 | ln -s $readme $out/README.md 110 | ''; 111 | 112 | rust-w64 = { pkgsCross'mingwW64 }: import inputs.rust { inherit (pkgsCross'mingwW64) pkgs; }; 113 | rust-w64-overlay = { rust-w64 }: let 114 | target = rust-w64.lib.rustTargetEnvironment { 115 | inherit (rust-w64) pkgs; 116 | rustcFlags = [ "-L native=${rust-w64.pkgs.windows.pthreads}/lib" ]; 117 | }; 118 | in cself: csuper: { 119 | sysroot-std = csuper.sysroot-std ++ [ cself.manifest.targets.${target.triple}.rust-std ]; 120 | cargo-cc = csuper.cargo-cc // cself.context.rlib.cargoEnv { 121 | inherit target; 122 | }; 123 | rustc-cc = csuper.rustc-cc // cself.context.rlib.rustcCcEnv { 124 | inherit target; 125 | }; 126 | }; 127 | 128 | generate = { rust'builders, outputHashes }: rust'builders.generateFiles { 129 | paths = { 130 | "lock.nix" = outputHashes; 131 | }; 132 | }; 133 | outputHashes = { rust'builders }: rust'builders.cargoOutputHashes { 134 | inherit crate; 135 | }; 136 | }; 137 | checks = with nixlib; { 138 | versions = { rust'builders, source }: rust'builders.check-contents { 139 | src = source; 140 | patterns = [ 141 | { path = "README.md"; 142 | plain = ''version = "${versions.majorMinor crate.members.qapi.version}"''; 143 | } 144 | ] ++ mapAttrsToList (dir: crate: { 145 | path = "${dir}/src/lib.rs"; 146 | docs'rs = { inherit (crate) name version; }; 147 | }) libs; 148 | }; 149 | } // mapAttrs' (dir: crate: nameValuePair "test-${dir}" (testCrate dir { })) libs 150 | // mapAttrs' (name: buildFeatures: nameValuePair "test-qapi-${name}" (testCrate "qapi" { 151 | inherit buildFeatures; 152 | })) featureMatrix; 153 | lib = { 154 | crate = rust.lib.importCargo { 155 | inherit self; 156 | path = ./Cargo.toml; 157 | inherit (import ./lock.nix) outputHashes; 158 | }; 159 | inherit (crate.package) version; 160 | }; 161 | config = rec { 162 | name = "qapi-rs"; 163 | packages.namespace = [ name ]; 164 | }; 165 | }; 166 | } 167 | -------------------------------------------------------------------------------- /qapi/src/futures/tokio.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | use std::sync::Arc; 5 | use futures::Stream; 6 | #[cfg(any(feature = "qapi-qmp", feature = "qapi-qga"))] 7 | use futures::Sink; 8 | use tokio::io::{AsyncRead, AsyncWrite, ReadHalf, WriteHalf, split}; 9 | use tokio_util::codec::{Framed, FramedParts}; 10 | use qapi_spec::{Response, Any}; 11 | #[cfg(any(feature = "qapi-qmp", feature = "qapi-qga"))] 12 | use qapi_spec::Execute; 13 | #[cfg(feature = "qapi-qmp")] 14 | use qapi_qmp::{QmpMessageAny, QmpCommand, QapiCapabilities, QMPCapability}; 15 | #[cfg(feature = "qapi-qmp")] 16 | use super::QmpStreamNegotiation; 17 | use super::{codec::JsonLinesCodec, QapiEvents, QapiService, QapiStream, QapiShared}; 18 | 19 | pub struct QgaStreamTokio { 20 | stream: Framed>> 21 | } 22 | 23 | impl QgaStreamTokio { 24 | fn new(stream: S) -> Self { 25 | Self { 26 | stream: Framed::from_parts(FramedParts::new::<()>(stream, JsonLinesCodec::new())), 27 | } 28 | } 29 | 30 | fn pair(self, write: W) -> QapiStream { 31 | let shared = Arc::new(QapiShared::new(false)); 32 | let events = QapiEvents { 33 | stream: self, 34 | shared: shared.clone(), 35 | }; 36 | let service = QapiService::new(write, shared); 37 | QapiStream { 38 | service, 39 | events, 40 | } 41 | } 42 | 43 | pub fn open_split(read: S, write: W) -> QapiStream> { 44 | let r = Self::new(read); 45 | let w = QgaStreamTokio::new(write); 46 | 47 | r.pair(w) 48 | } 49 | } 50 | 51 | impl QgaStreamTokio> { 52 | pub fn open(stream: R) -> QapiStream>> where 53 | R: AsyncRead + AsyncWrite, 54 | { 55 | let (r, w) = split(stream); 56 | let r = Self::new(r); 57 | let w = QgaStreamTokio::new(w); 58 | 59 | r.pair(w) 60 | } 61 | } 62 | 63 | #[cfg(all(unix, feature = "async-tokio-net"))] 64 | impl QgaStreamTokio> { 65 | pub async fn open_uds>(socket_addr: P) -> io::Result>>> { 66 | let socket = tokio::net::UnixStream::connect(socket_addr).await?; 67 | let (r, w) = split(socket); 68 | Ok(Self::open_split(r, w)) 69 | } 70 | } 71 | 72 | #[cfg(feature = "async-tokio-net")] 73 | impl QgaStreamTokio> { 74 | pub async fn open_tcp(socket_addr: A) -> io::Result>>> { 75 | let socket = tokio::net::TcpStream::connect(socket_addr).await?; 76 | let (r, w) = split(socket); 77 | Ok(Self::open_split(r, w)) 78 | } 79 | } 80 | 81 | impl QgaStreamTokio { 82 | fn stream(self: Pin<&mut Self>) -> Pin<&mut Framed>>> { 83 | unsafe { 84 | self.map_unchecked_mut(|this| &mut this.stream) 85 | } 86 | } 87 | } 88 | 89 | impl Stream for QgaStreamTokio { 90 | type Item = io::Result>; 91 | 92 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 93 | self.stream().poll_next(cx) 94 | } 95 | } 96 | 97 | #[cfg(feature = "qapi-qga")] 98 | impl Sink> for QgaStreamTokio { 99 | type Error = io::Error; 100 | 101 | fn start_send(self: Pin<&mut Self>, item: Execute) -> Result<(), Self::Error> { 102 | self.stream().start_send(item) 103 | } 104 | 105 | fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 106 | Sink::>::poll_ready(self.stream(), cx) 107 | } 108 | 109 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 110 | Sink::>::poll_flush(self.stream(), cx) 111 | } 112 | 113 | fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 114 | Sink::>::poll_close(self.stream(), cx) 115 | } 116 | } 117 | 118 | #[cfg(feature = "qapi-qmp")] 119 | pub struct QmpStreamTokio { 120 | stream: Framed>, 121 | } 122 | 123 | #[cfg(feature = "qapi-qmp")] 124 | impl QmpStreamTokio { 125 | fn stream(self: Pin<&mut Self>) -> Pin<&mut Framed>> { 126 | unsafe { 127 | self.map_unchecked_mut(|this| &mut this.stream) 128 | } 129 | } 130 | } 131 | 132 | #[cfg(feature = "qapi-qmp")] 133 | impl Stream for QmpStreamTokio { 134 | type Item = io::Result; 135 | 136 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 137 | self.stream().poll_next(cx) 138 | } 139 | } 140 | 141 | #[cfg(feature = "qapi-qmp")] 142 | impl Sink> for QmpStreamTokio { 143 | type Error = io::Error; 144 | 145 | fn start_send(self: Pin<&mut Self>, item: Execute) -> Result<(), Self::Error> { 146 | self.stream().start_send(item) 147 | } 148 | 149 | fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 150 | Sink::>::poll_ready(self.stream(), cx) 151 | } 152 | 153 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 154 | Sink::>::poll_flush(self.stream(), cx) 155 | } 156 | 157 | fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 158 | Sink::>::poll_close(self.stream(), cx) 159 | } 160 | } 161 | 162 | #[cfg(feature = "qapi-qmp")] 163 | impl QmpStreamTokio { 164 | pub fn new(stream: S) -> Self { 165 | Self { 166 | stream: Framed::from_parts(FramedParts::new::<()>(stream, JsonLinesCodec::::new())), 167 | } 168 | } 169 | 170 | pub async fn open_split(read: S, write: W) -> io::Result>> where 171 | S: AsyncRead + Unpin, 172 | { 173 | use futures::StreamExt; 174 | 175 | let mut lines = Framed::from_parts(FramedParts::new::<()>(read, JsonLinesCodec::::new())); 176 | 177 | let capabilities = lines.next().await.ok_or_else(|| 178 | io::Error::new(io::ErrorKind::UnexpectedEof, "QMP greeting expected") 179 | )??; 180 | 181 | let lines = lines.into_parts(); 182 | let mut read = FramedParts::new::<()>(lines.io, JsonLinesCodec::new()); 183 | read.read_buf = lines.read_buf; 184 | let stream = Framed::from_parts(read); 185 | 186 | let supports_oob = capabilities.capabilities().any(|c| c == QMPCapability::oob); 187 | let shared = Arc::new(QapiShared::new(supports_oob)); 188 | let events = QapiEvents { 189 | stream: Self { stream }, 190 | shared: shared.clone(), 191 | }; 192 | let service = QapiService::new(QmpStreamTokio::new(write), shared); 193 | 194 | Ok(QmpStreamNegotiation { 195 | stream: QapiStream { 196 | service, 197 | events, 198 | }, 199 | capabilities, 200 | }) 201 | } 202 | } 203 | 204 | #[cfg(feature = "qapi-qmp")] 205 | impl QmpStreamTokio> { 206 | pub async fn open(stream: RW) -> io::Result>>> where RW: Unpin { 207 | let (r, w) = split(stream); 208 | Self::open_split(r, w).await 209 | } 210 | } 211 | 212 | #[cfg(all(unix, feature = "qapi-qmp", feature = "async-tokio-net"))] 213 | impl QmpStreamTokio> { 214 | pub async fn open_uds>(socket_addr: P) -> io::Result>>> { 215 | let socket = tokio::net::UnixStream::connect(socket_addr).await?; 216 | let (r, w) = split(socket); 217 | Self::open_split(r, w).await 218 | } 219 | } 220 | 221 | #[cfg(all(feature = "qapi-qmp", feature = "async-tokio-net"))] 222 | impl QmpStreamTokio> { 223 | pub async fn open_tcp(socket_addr: A) -> io::Result>>> { 224 | let socket = tokio::net::TcpStream::connect(socket_addr).await?; 225 | let (r, w) = split(socket); 226 | Self::open_split(r, w).await 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /spec/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_root_url = "https://docs.rs/qapi-spec/0.3.2")] 2 | 3 | use std::{io, error, fmt, str}; 4 | use std::marker::PhantomData; 5 | use serde::{Serialize, Serializer, Deserialize, Deserializer}; 6 | use serde::de::DeserializeOwned; 7 | 8 | pub use serde_json::Value as Any; 9 | pub type Dictionary = serde_json::Map; 10 | 11 | #[derive(Debug, Copy, Clone, Serialize, Deserialize)] 12 | pub struct Empty { } 13 | 14 | pub enum Never { } 15 | 16 | impl Serialize for Never { 17 | fn serialize(&self, _: S) -> Result { 18 | match *self { } 19 | } 20 | } 21 | 22 | impl<'de> Deserialize<'de> for Never { 23 | fn deserialize>(_: D) -> Result { 24 | use serde::de::Error; 25 | 26 | Err(D::Error::custom("Cannot instantiate Never type")) 27 | } 28 | } 29 | 30 | #[doc(hidden)] 31 | pub mod base64 { 32 | use serde::{Serialize, Serializer, Deserialize, Deserializer}; 33 | use serde::de::{Error, Unexpected}; 34 | use base64::{prelude::*, DecodeError}; 35 | 36 | pub fn serialize(data: &[u8], serializer: S) -> Result { 37 | BASE64_STANDARD.encode(data).serialize(serializer) 38 | } 39 | 40 | pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { 41 | // TODO: deserialize to borrowed &str 42 | let str = String::deserialize(deserializer)?; 43 | 44 | BASE64_STANDARD.decode(&str) 45 | .map_err(|e| de_err(&str, e)) 46 | } 47 | 48 | pub fn de_err(str: &str, err: DecodeError) -> E { 49 | match err { 50 | DecodeError::InvalidByte(..) | DecodeError::InvalidPadding => 51 | E::invalid_value(Unexpected::Str(str), &"base64"), 52 | DecodeError::InvalidLength(len) => 53 | E::invalid_length(len, &"valid base64 length"), 54 | DecodeError::InvalidLastSymbol(..) => 55 | E::invalid_value(Unexpected::Str(str), &"truncated or corrupted base64"), 56 | } 57 | } 58 | } 59 | 60 | #[doc(hidden)] 61 | pub mod base64_opt { 62 | use serde::{Serializer, Deserialize, Deserializer}; 63 | use crate::base64; 64 | use ::base64::prelude::*; 65 | 66 | pub fn serialize(data: &Option>, serializer: S) -> Result { 67 | base64::serialize(data.as_ref().expect("use skip_serializing_with"), serializer) 68 | } 69 | 70 | pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result>, D::Error> { 71 | // TODO: deserialize to borrowed &str 72 | let str = >::deserialize(deserializer)?; 73 | if let Some(ref str) = str { 74 | BASE64_STANDARD.decode(str) 75 | .map(Some) 76 | .map_err(|e| base64::de_err(str, e)) 77 | } else { 78 | Ok(None) 79 | } 80 | } 81 | } 82 | 83 | mod error_serde { 84 | use serde::{Serialize, Serializer, Deserialize, Deserializer}; 85 | use crate::{Error, ErrorClass, Any}; 86 | 87 | #[derive(Deserialize)] 88 | pub struct ErrorValue { 89 | pub class: ErrorClass, 90 | pub desc: String, 91 | } 92 | 93 | #[derive(Deserialize)] 94 | struct QapiError { 95 | error: ErrorValue, 96 | #[serde(default, skip_serializing_if = "Option::is_none")] 97 | id: Option, 98 | } 99 | 100 | #[derive(Serialize)] 101 | pub struct ErrorValueSer<'a> { 102 | pub class: &'a ErrorClass, 103 | pub desc: &'a str, 104 | } 105 | 106 | #[derive(Serialize)] 107 | struct QapiErrorSer<'a> { 108 | error: ErrorValueSer<'a>, 109 | #[serde(default, skip_serializing_if = "Option::is_none")] 110 | id: Option<&'a Any>, 111 | } 112 | 113 | impl Serialize for Error { 114 | fn serialize(&self, serializer: S) -> Result { 115 | QapiErrorSer { 116 | error: ErrorValueSer { 117 | class: &self.class, 118 | desc: &self.desc[..], 119 | }, 120 | id: self.id.as_ref(), 121 | }.serialize(serializer) 122 | } 123 | } 124 | 125 | impl<'de> Deserialize<'de> for Error { 126 | fn deserialize>(deserializer: D) -> Result { 127 | QapiError::deserialize(deserializer).map(|e| Error { 128 | class: e.error.class, 129 | desc: e.error.desc, 130 | id: e.id, 131 | }) 132 | } 133 | } 134 | } 135 | 136 | #[derive(Debug, Clone, Serialize, Deserialize)] 137 | pub struct ResponseValue { 138 | #[serde(rename = "return")] 139 | return_: C, 140 | #[serde(default, skip_serializing_if = "Option::is_none")] 141 | id: Option, 142 | } 143 | 144 | #[derive(Debug, Clone, Serialize, Deserialize)] 145 | #[serde(untagged)] 146 | pub enum Response { 147 | Err(Error), 148 | Ok(ResponseValue), 149 | } 150 | 151 | impl Response { 152 | pub fn result(self) -> Result { 153 | match self { 154 | Response::Ok(ResponseValue { return_, .. }) => Ok(return_), 155 | Response::Err(e) => Err(e), 156 | } 157 | } 158 | 159 | pub fn id(&self) -> Option<&Any> { 160 | match self { 161 | Response::Err(err) => err.id.as_ref(), 162 | Response::Ok(value) => value.id.as_ref(), 163 | } 164 | } 165 | } 166 | 167 | pub trait Command: Serialize + Sync + Send { 168 | type Ok: DeserializeOwned; 169 | 170 | const NAME: &'static str; 171 | const ALLOW_OOB: bool; 172 | } 173 | 174 | impl<'a, C: Command> Command for &'a C { 175 | type Ok = C::Ok; 176 | 177 | const NAME: &'static str = C::NAME; 178 | const ALLOW_OOB: bool = C::ALLOW_OOB; 179 | } 180 | 181 | impl<'a, C: Command> Command for &'a mut C { 182 | type Ok = C::Ok; 183 | 184 | const NAME: &'static str = C::NAME; 185 | const ALLOW_OOB: bool = C::ALLOW_OOB; 186 | } 187 | 188 | pub trait Event: DeserializeOwned { 189 | const NAME: &'static str; 190 | } 191 | 192 | pub unsafe trait Enum: DeserializeOwned + str::FromStr + Copy + 'static { 193 | fn discriminant(&self) -> usize; 194 | 195 | fn name(&self) -> &'static str { 196 | unsafe { 197 | Self::NAMES.get_unchecked(self.discriminant()) 198 | } 199 | } 200 | 201 | fn from_name(s: &str) -> Option { 202 | Self::NAMES.iter().zip(Self::VARIANTS) 203 | .find(|&(&n, _)| n == s) 204 | .map(|(_, &v)| v) 205 | } 206 | 207 | const COUNT: usize; 208 | const VARIANTS: &'static [Self]; 209 | const NAMES: &'static [&'static str]; 210 | } 211 | 212 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 213 | pub enum ErrorClass { 214 | /// this is used for errors that don’t require a specific error class. This should be the default case for most errors 215 | GenericError, 216 | /// the requested command has not been found 217 | CommandNotFound, 218 | /// a device has failed to be become active 219 | DeviceNotActive, 220 | /// the requested device has not been found 221 | DeviceNotFound, 222 | /// the requested operation can’t be fulfilled because a required KVM capability is missing 223 | KVMMissingCap, 224 | } 225 | 226 | impl From for io::ErrorKind { 227 | fn from(e: ErrorClass) -> Self { 228 | match e { 229 | ErrorClass::GenericError => io::ErrorKind::Other, 230 | ErrorClass::CommandNotFound => io::ErrorKind::InvalidInput, 231 | ErrorClass::DeviceNotActive => io::ErrorKind::Other, 232 | ErrorClass::DeviceNotFound => io::ErrorKind::NotFound, 233 | ErrorClass::KVMMissingCap => io::ErrorKind::Other, 234 | } 235 | } 236 | } 237 | 238 | #[derive(Debug, Clone)] 239 | pub struct Error { 240 | pub class: ErrorClass, 241 | pub desc: String, 242 | pub id: Option, 243 | } 244 | 245 | pub type CommandResult = Result<::Ok, Error>; 246 | 247 | fn serialize_command_name(_: &PhantomData<&'static str>, s: S) -> Result { 248 | C::NAME.serialize(s) 249 | } 250 | 251 | #[derive(Serialize)] 252 | pub struct Execute { 253 | #[serde(serialize_with = "serialize_command_name::", bound = "C: Command")] 254 | pub execute: PhantomData<&'static str>, 255 | pub arguments: C, 256 | #[serde(skip_serializing_if = "Option::is_none")] 257 | pub id: Option, 258 | } 259 | 260 | #[derive(Serialize)] 261 | pub struct ExecuteOob { 262 | #[serde(rename = "exec-oob", serialize_with = "serialize_command_name::", bound = "C: Command")] 263 | pub execute_oob: PhantomData<&'static str>, 264 | pub arguments: C, 265 | pub id: I, 266 | } 267 | 268 | impl Execute { 269 | pub fn new(arguments: C, id: Option) -> Self { 270 | Self { 271 | execute: PhantomData, 272 | arguments, 273 | id, 274 | } 275 | } 276 | 277 | pub fn with_command(arguments: C) -> Self { 278 | Self { 279 | execute: PhantomData, 280 | arguments, 281 | id: None, 282 | } 283 | } 284 | 285 | pub fn with_id(arguments: C, id: I) -> Self { 286 | Self { 287 | execute: PhantomData, 288 | arguments, 289 | id: Some(id), 290 | } 291 | } 292 | } 293 | 294 | impl From for Execute { 295 | fn from(command: C) -> Self { 296 | Self::with_command(command) 297 | } 298 | } 299 | 300 | impl ExecuteOob { 301 | pub fn new(arguments: C, id: I) -> Self { 302 | Self { 303 | execute_oob: PhantomData, 304 | arguments, 305 | id, 306 | } 307 | } 308 | } 309 | 310 | impl error::Error for Error { 311 | fn description(&self) -> &str { 312 | &self.desc 313 | } 314 | } 315 | 316 | impl fmt::Display for Error { 317 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 318 | fmt::Display::fmt(&self.desc, f) 319 | } 320 | } 321 | 322 | impl From for io::Error { 323 | fn from(e: Error) -> Self { 324 | io::Error::new(e.class.into(), e.desc) 325 | } 326 | } 327 | 328 | #[derive(Debug, Copy, Clone, Serialize, Deserialize)] 329 | pub struct Timestamp { 330 | seconds: u64, 331 | microseconds: u64, 332 | } 333 | -------------------------------------------------------------------------------- /qapi/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_root_url = "https://docs.rs/qapi/0.15.0")] 2 | 3 | #[cfg(feature = "qapi-qmp")] 4 | pub use qapi_qmp as qmp; 5 | 6 | #[cfg(feature = "qapi-qga")] 7 | pub use qapi_qga as qga; 8 | 9 | pub use qapi_spec::{Any, Dictionary, Empty, Never, Execute, ExecuteOob, Command, CommandResult, Event, Enum, Error, ErrorClass, Timestamp}; 10 | 11 | pub use self::stream::Stream; 12 | 13 | #[cfg(feature = "qapi-qmp")] 14 | pub use self::qmp_impl::*; 15 | 16 | #[cfg(feature = "qapi-qga")] 17 | pub use self::qga_impl::*; 18 | 19 | use std::{error, fmt, io}; 20 | 21 | #[cfg(feature = "async")] 22 | pub mod futures; 23 | 24 | #[derive(Debug)] 25 | pub enum ExecuteError { 26 | Qapi(Error), 27 | Io(io::Error), 28 | } 29 | 30 | pub type ExecuteResult = Result<::Ok, ExecuteError>; 31 | 32 | impl fmt::Display for ExecuteError { 33 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 34 | match self { 35 | ExecuteError::Qapi(e) => fmt::Display::fmt(e, f), 36 | ExecuteError::Io(e) => fmt::Display::fmt(e, f), 37 | } 38 | } 39 | } 40 | 41 | impl error::Error for ExecuteError { 42 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 43 | match self { 44 | ExecuteError::Qapi(e) => Some(e), 45 | ExecuteError::Io(e) => Some(e), 46 | } 47 | } 48 | } 49 | 50 | impl From for ExecuteError { 51 | fn from(e: io::Error) -> Self { 52 | ExecuteError::Io(e) 53 | } 54 | } 55 | 56 | impl From for ExecuteError { 57 | fn from(e: Error) -> Self { 58 | ExecuteError::Qapi(e) 59 | } 60 | } 61 | 62 | impl From for io::Error { 63 | fn from(e: ExecuteError) -> Self { 64 | match e { 65 | ExecuteError::Qapi(e) => e.into(), 66 | ExecuteError::Io(e) => e, 67 | } 68 | } 69 | } 70 | 71 | #[cfg(any(feature = "qapi-qmp", feature = "qapi-qga"))] 72 | mod qapi { 73 | use serde_json; 74 | use serde::{Serialize, Deserialize}; 75 | use std::io::{self, BufRead, Write}; 76 | use crate::{Command, Execute}; 77 | use log::trace; 78 | 79 | pub struct Qapi { 80 | pub stream: S, 81 | pub buffer: Vec, 82 | } 83 | 84 | impl Qapi { 85 | pub fn new(s: S) -> Self { 86 | Qapi { 87 | stream: s, 88 | buffer: Default::default(), 89 | } 90 | } 91 | } 92 | 93 | impl Qapi { 94 | pub fn decode_line<'de, D: Deserialize<'de>>(&'de mut self) -> io::Result> { 95 | self.buffer.clear(); 96 | let line = self.stream.read_until(b'\n', &mut self.buffer)?; 97 | let line = &self.buffer[..line]; 98 | trace!("<- {}", String::from_utf8_lossy(line)); 99 | 100 | if line.is_empty() { 101 | Ok(None) 102 | } else { 103 | serde_json::from_slice(line).map(Some).map_err(From::from) 104 | } 105 | } 106 | } 107 | 108 | impl Qapi { 109 | pub fn encode_line(&mut self, command: &C) -> io::Result<()> { 110 | { 111 | let mut ser = serde_json::Serializer::new(&mut self.stream); 112 | command.serialize(&mut ser)?; 113 | } 114 | 115 | self.stream.write(&[b'\n'])?; 116 | 117 | self.stream.flush() 118 | } 119 | 120 | pub fn write_command(&mut self, command: &C) -> io::Result<()> { 121 | self.encode_line(&Execute::<&C>::from(command))?; 122 | 123 | trace!("-> execute {}: {}", C::NAME, serde_json::to_string_pretty(command).unwrap()); 124 | 125 | Ok(()) 126 | } 127 | } 128 | } 129 | 130 | mod stream { 131 | use std::io::{Read, Write, BufRead, Result}; 132 | 133 | pub struct Stream { 134 | r: R, 135 | w: W, 136 | } 137 | 138 | impl Stream { 139 | pub fn new(r: R, w: W) -> Self { 140 | Stream { 141 | r, 142 | w, 143 | } 144 | } 145 | 146 | pub fn into_inner(self) -> (R, W) { 147 | (self.r, self.w) 148 | } 149 | 150 | pub fn get_ref_read(&self) -> &R { &self.r } 151 | pub fn get_mut_read(&mut self) -> &mut R { &mut self.r } 152 | pub fn get_ref_write(&self) -> &W { &self.w } 153 | pub fn get_mut_write(&mut self) -> &mut W { &mut self.w } 154 | } 155 | 156 | impl Read for Stream { 157 | fn read(&mut self, buf: &mut [u8]) -> Result { 158 | self.r.read(buf) 159 | } 160 | } 161 | 162 | impl BufRead for Stream { 163 | fn fill_buf(&mut self) -> Result<&[u8]> { 164 | self.r.fill_buf() 165 | } 166 | 167 | fn consume(&mut self, amt: usize) { 168 | self.r.consume(amt) 169 | } 170 | } 171 | 172 | impl Write for Stream { 173 | fn write(&mut self, buf: &[u8]) -> Result { 174 | self.w.write(buf) 175 | } 176 | 177 | fn flush(&mut self) -> Result<()> { 178 | self.w.flush() 179 | } 180 | } 181 | } 182 | 183 | #[cfg(feature = "qapi-qmp")] 184 | mod qmp_impl { 185 | use std::io::{self, BufRead, Read, Write, BufReader}; 186 | use std::vec::Drain; 187 | use qapi_qmp::{QMP, QapiCapabilities, QmpMessage, Event, qmp_capabilities, query_version}; 188 | use crate::{qapi::Qapi, Stream, ExecuteResult, ExecuteError, Command}; 189 | 190 | pub struct Qmp { 191 | inner: Qapi, 192 | event_queue: Vec, 193 | } 194 | 195 | impl Qmp, S>> { 196 | pub fn from_stream(s: S) -> Self { 197 | Self::new(Stream::new(BufReader::new(s.clone()), s)) 198 | } 199 | } 200 | 201 | impl Qmp { 202 | pub fn new(stream: S) -> Self { 203 | Qmp { 204 | inner: Qapi::new(stream), 205 | event_queue: Default::default(), 206 | } 207 | } 208 | 209 | pub fn into_inner(self) -> S { 210 | self.inner.stream 211 | } 212 | 213 | pub fn inner(&self) -> &S { 214 | &self.inner.stream 215 | } 216 | 217 | pub fn inner_mut(&mut self) -> &mut S { 218 | &mut self.inner.stream 219 | } 220 | 221 | pub fn events(&mut self) -> Drain<'_, Event> { 222 | self.event_queue.drain(..) 223 | } 224 | } 225 | 226 | impl Qmp { 227 | pub fn read_capabilities(&mut self) -> io::Result { 228 | self.inner.decode_line().map(|v: Option| 229 | v.expect("unexpected eof").QMP 230 | ) 231 | } 232 | 233 | pub fn read_response(&mut self) -> ExecuteResult { 234 | loop { 235 | match self.inner.decode_line()? { 236 | None => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "expected command response").into()), 237 | Some(QmpMessage::Response(res)) => return res.result().map_err(From::from), 238 | Some(QmpMessage::Event(e)) => self.event_queue.push(e), 239 | } 240 | } 241 | } 242 | } 243 | 244 | impl Qmp { 245 | pub fn write_command(&mut self, command: &C) -> io::Result<()> { 246 | self.inner.write_command(command) 247 | } 248 | 249 | pub fn execute(&mut self, command: &C) -> ExecuteResult { 250 | self.write_command(command)?; 251 | self.read_response::() 252 | } 253 | 254 | pub fn handshake(&mut self) -> Result { 255 | let caps = self.read_capabilities()?; 256 | self.execute(&qmp_capabilities { enable: None }) 257 | .map(|_| caps) 258 | } 259 | 260 | /// Can be used to poll the socket for pending events 261 | pub fn nop(&mut self) -> io::Result<()> { 262 | self.execute(&query_version { }) 263 | .map_err(From::from) 264 | .map(drop) 265 | } 266 | } 267 | } 268 | 269 | #[cfg(feature = "qapi-qga")] 270 | mod qga_impl { 271 | use std::io::{self, BufRead, Read, Write, BufReader}; 272 | use qapi_qga::guest_sync; 273 | use qapi_spec::Response; 274 | use crate::{qapi::Qapi, Stream, Command, ExecuteResult, ExecuteError}; 275 | 276 | pub struct Qga { 277 | inner: Qapi, 278 | } 279 | 280 | impl Qga, S>> { 281 | pub fn from_stream(s: S) -> Self { 282 | Self::new(Stream::new(BufReader::new(s.clone()), s)) 283 | } 284 | } 285 | 286 | impl Qga { 287 | pub fn new(stream: S) -> Self { 288 | Qga { 289 | inner: Qapi::new(stream), 290 | } 291 | } 292 | 293 | pub fn into_inner(self) -> S { 294 | self.inner.stream 295 | } 296 | 297 | pub fn inner(&self) -> &S { 298 | &self.inner.stream 299 | } 300 | 301 | pub fn inner_mut(&mut self) -> &mut S { 302 | &mut self.inner.stream 303 | } 304 | } 305 | 306 | impl Qga { 307 | pub fn read_response(&mut self) -> ExecuteResult { 308 | loop { 309 | match self.inner.decode_line()?.map(|r: Response<_>| r.result()) { 310 | None => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "expected command response").into()), 311 | Some(Ok(res)) => return Ok(res), 312 | Some(Err(e)) => return Err(e.into()), 313 | } 314 | } 315 | } 316 | } 317 | 318 | impl Qga { 319 | pub fn write_command(&mut self, command: &C) -> io::Result<()> { 320 | self.inner.write_command(command) 321 | } 322 | 323 | pub fn execute(&mut self, command: &C) -> ExecuteResult { 324 | self.write_command(command)?; 325 | self.read_response::() 326 | } 327 | 328 | pub fn guest_sync(&mut self, sync_value: i32) -> Result<(), ExecuteError> { 329 | let id = sync_value.into(); 330 | let sync = guest_sync { 331 | id, 332 | }; 333 | 334 | match self.execute(&sync) { 335 | Ok(r) if r == sync.id => Ok(()), 336 | Ok(..) => Err(io::Error::new(io::ErrorKind::InvalidData, "guest-sync handshake failed").into()), 337 | Err(e) => Err(e.into()), 338 | } 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /qapi/src/futures/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "qapi-qmp")] 2 | use qapi_qmp::{QmpMessage, QmpMessageAny, QapiCapabilities, QMPCapability}; 3 | 4 | use qapi_spec::Response; 5 | use crate::{Any, Execute, ExecuteResult, Command}; 6 | 7 | use std::collections::BTreeMap; 8 | use std::convert::TryInto; 9 | use std::marker::Unpin; 10 | use std::sync::{Arc, Mutex as StdMutex, atomic::{AtomicUsize, AtomicBool, Ordering}}; 11 | use std::task::{Context, Poll}; 12 | use std::pin::Pin; 13 | use std::io; 14 | use futures::channel::oneshot; 15 | use futures::task::AtomicWaker; 16 | use futures::lock::Mutex; 17 | use futures::{Future, FutureExt, Sink, SinkExt, Stream}; 18 | use serde::Deserialize; 19 | use log::{trace, info, warn}; 20 | 21 | #[cfg(feature = "tokio-util")] 22 | mod codec; 23 | 24 | #[cfg(feature = "tokio")] 25 | mod tokio; 26 | #[cfg(feature = "tokio")] 27 | pub use self::tokio::*; 28 | 29 | #[cfg(feature = "tower-service")] 30 | mod tower; 31 | 32 | pub struct QapiStream { 33 | service: QapiService, 34 | events: QapiEvents, 35 | } 36 | 37 | impl QapiStream { 38 | pub fn with_parts(service: QapiService, events: QapiEvents) -> Self { 39 | Self { 40 | service, 41 | events, 42 | } 43 | } 44 | 45 | pub fn into_parts(self) -> (QapiService, QapiEvents) { 46 | (self.service, self.events) 47 | } 48 | 49 | #[cfg(feature = "async-tokio-spawn")] 50 | pub fn spawn_tokio(self) -> (QapiService, ::tokio::task::JoinHandle<()>) where 51 | QapiEvents: Future> + Send + 'static, 52 | { 53 | let handle = self.events.spawn_tokio(); 54 | (self.service, handle) 55 | } 56 | 57 | pub fn execute<'a, C: Command + 'a>(&'a mut self, command: C) -> impl Future> + 'a where 58 | QapiEvents: Future> + Unpin, 59 | W: Sink, Error=io::Error> + Unpin 60 | { 61 | let execute = self.service.execute(command).fuse(); 62 | 63 | async move { 64 | futures::pin_mut!(execute); 65 | 66 | futures::select_biased! { 67 | res = execute => res, 68 | res = (&mut self.events).fuse() => { 69 | res?; 70 | Err(io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected EOF when executing command").into()) 71 | }, 72 | } 73 | } 74 | } 75 | } 76 | 77 | #[cfg(feature = "qapi-qmp")] 78 | pub struct QmpStreamNegotiation { 79 | pub stream: QapiStream, 80 | pub capabilities: QapiCapabilities, 81 | } 82 | 83 | #[cfg(feature = "qapi-qmp")] 84 | impl QmpStreamNegotiation where 85 | QapiEvents: Future> + Unpin, 86 | W: Sink, Error=io::Error> + Unpin, 87 | { 88 | pub async fn negotiate_caps(mut self, caps: C) -> io::Result> where 89 | C: IntoIterator, 90 | { 91 | let _ = self.stream.execute(qapi_qmp::qmp_capabilities { 92 | enable: Some(caps.into_iter().collect()), 93 | }).await?; 94 | 95 | Ok(self.stream) 96 | } 97 | 98 | pub async fn negotiate(self) -> io::Result> { 99 | self.negotiate_caps(std::iter::empty()).await 100 | } 101 | } 102 | 103 | type QapiCommandMap = BTreeMap>>; 104 | 105 | pub struct QapiService { 106 | shared: Arc, 107 | write: Arc>, 108 | id_counter: AtomicUsize, 109 | } 110 | 111 | impl QapiService { 112 | #[cfg(feature = "tokio")] 113 | fn new(write: W, shared: Arc) -> Self { 114 | QapiService { 115 | shared, 116 | write: Mutex::new(write).into(), 117 | id_counter: AtomicUsize::new(0), 118 | } 119 | } 120 | 121 | fn next_oob_id(&self) -> u32 { 122 | self.id_counter.fetch_add(1, Ordering::Relaxed) as _ 123 | } 124 | 125 | fn command_id(&self) -> Option { 126 | if self.shared.supports_oob { 127 | Some(self.next_oob_id()) 128 | } else { 129 | None 130 | } 131 | } 132 | 133 | fn command_response(receiver: oneshot::Receiver>) -> impl Future> { 134 | receiver.map(|res| match res { 135 | Ok(Ok(res)) => C::Ok::deserialize(&res) 136 | .map_err(io::Error::from).map_err(From::from), 137 | Ok(Err(e)) => Err(e.into()), 138 | Err(_cancelled) => Err(io::Error::new(io::ErrorKind::UnexpectedEof, "QAPI stream disconnected").into()), 139 | }) 140 | } 141 | 142 | pub fn execute(&self, command: C) -> impl Future> where 143 | W: Sink, Error=io::Error> + Unpin 144 | { 145 | let id = self.command_id(); 146 | let sink = self.write.clone(); 147 | let shared = self.shared.clone(); 148 | let command = Execute::new(command, id); 149 | 150 | async move { 151 | let mut sink = sink.lock().await; 152 | let receiver = shared.command_insert(id.unwrap_or_default()); 153 | 154 | sink.send(command).await?; 155 | if id.is_some() { 156 | // retain write lock only if id/oob execution isn't supported 157 | drop(sink) 158 | } 159 | 160 | Self::command_response::(receiver).await 161 | } 162 | } 163 | 164 | /*pub async fn execute_oob(&self, command: C) -> io::Result> { 165 | /* TODO: should we assert C::ALLOW_OOB here and/or at the type level? 166 | * If oob isn't supported should we fall back to serial execution or error? 167 | */ 168 | self.execute_(command, true).await 169 | }*/ 170 | 171 | #[cfg(feature = "qapi-qga")] 172 | pub fn guest_sync(&self, sync_value: i32) -> impl Future> where 173 | W: Sink, Error=io::Error> + Unpin 174 | { 175 | let id = sync_value.into(); 176 | self.execute(qapi_qga::guest_sync { 177 | id, 178 | }).map(move |res| res.and_then(|res| if res == id { 179 | Ok(()) 180 | } else { 181 | Err(io::Error::new(io::ErrorKind::InvalidData, "QGA sync failed").into()) 182 | })) 183 | } 184 | 185 | fn stop(&self) { 186 | let mut commands = self.shared.commands.lock().unwrap(); 187 | if self.shared.abandoned.load(Ordering::Relaxed) { 188 | self.shared.stop(); 189 | } 190 | commands.abandoned = true; 191 | } 192 | } 193 | 194 | impl Drop for QapiService { 195 | fn drop(&mut self) { 196 | self.stop(); 197 | } 198 | } 199 | 200 | #[derive(Default)] 201 | struct QapiSharedCommands { 202 | pending: QapiCommandMap, 203 | abandoned: bool, 204 | } 205 | 206 | struct QapiShared { 207 | commands: StdMutex, 208 | stop_waker: AtomicWaker, 209 | stop: AtomicBool, 210 | abandoned: AtomicBool, 211 | supports_oob: bool, 212 | } 213 | 214 | impl QapiShared { 215 | #[cfg(feature = "tokio")] 216 | fn new(supports_oob: bool) -> Self { 217 | Self { 218 | commands: Default::default(), 219 | stop_waker: Default::default(), 220 | stop: Default::default(), 221 | abandoned: Default::default(), 222 | supports_oob, 223 | } 224 | } 225 | 226 | fn stop(&self) { 227 | self.stop.store(true, Ordering::Relaxed); 228 | self.stop_waker.wake(); 229 | } 230 | 231 | fn is_stopped(&self) -> bool { 232 | self.stop.load(Ordering::Relaxed) 233 | } 234 | 235 | fn poll_next Poll>>(&self, cx: &mut Context, poll: P) -> Poll> { 236 | if self.is_stopped() { 237 | return Poll::Ready(None) 238 | } 239 | 240 | // attempt to complete the future 241 | match poll(cx) { 242 | Poll::Ready(res) => { 243 | if res.is_none() { 244 | self.stop.store(true, Ordering::Relaxed); 245 | } 246 | Poll::Ready(res) 247 | }, 248 | Poll::Pending => { 249 | self.stop_waker.register(cx.waker()); 250 | if self.is_stopped() { 251 | Poll::Ready(None) 252 | } else { 253 | Poll::Pending 254 | } 255 | }, 256 | } 257 | } 258 | 259 | fn command_remove(&self, id: u32) -> Option>> { 260 | let mut commands = self.commands.lock().unwrap(); 261 | commands.pending.remove(&id) 262 | } 263 | 264 | fn command_insert(&self, id: u32) -> oneshot::Receiver> { 265 | let (sender, receiver) = oneshot::channel(); 266 | let mut commands = self.commands.lock().unwrap(); 267 | if !commands.abandoned { 268 | // otherwise sender is dropped immediately 269 | if let Some(_prev) = commands.pending.insert(id, sender) { 270 | panic!("QAPI duplicate command id {:?}, this should not happen", id); 271 | } 272 | } 273 | receiver 274 | } 275 | } 276 | 277 | #[must_use] 278 | pub struct QapiEvents { 279 | stream: S, 280 | shared: Arc, 281 | } 282 | 283 | impl QapiEvents { 284 | pub fn release(&self) -> Result<(), ()> { 285 | let commands = self.shared.commands.lock().unwrap(); 286 | if commands.abandoned { 287 | Err(()) 288 | } else { 289 | self.shared.abandoned.store(true, Ordering::Relaxed); 290 | Ok(()) 291 | } 292 | } 293 | 294 | pub async fn into_future(self) -> () where 295 | Self: Future>, 296 | { 297 | if self.release().is_err() { 298 | info!("QAPI service abandoned before spawning"); 299 | return 300 | } 301 | 302 | match self.await { 303 | Ok(()) => (), 304 | Err(e) => 305 | warn!("QAPI stream closed with error {:?}", e), 306 | } 307 | } 308 | 309 | pub fn spawn(self, spawn: SP) -> Result<(), futures::task::SpawnError> where 310 | Self: Future> + Send + 'static, 311 | S: 'static 312 | { 313 | use futures::task::SpawnExt; 314 | 315 | spawn.spawn(self.into_future()) 316 | } 317 | 318 | #[cfg(feature = "async-tokio-spawn")] 319 | pub fn spawn_tokio(self) -> ::tokio::task::JoinHandle<()> where 320 | Self: Future> + Send + 'static, 321 | S: 'static 322 | { 323 | ::tokio::spawn(self.into_future()) 324 | } 325 | } 326 | 327 | impl Drop for QapiEvents { 328 | fn drop(&mut self) { 329 | let mut commands = self.shared.commands.lock().unwrap(); 330 | commands.pending.clear(); 331 | commands.abandoned = true; 332 | } 333 | } 334 | 335 | fn response_id(res: &Response, supports_oob: bool) -> io::Result { 336 | match (res.id().and_then(|id| id.as_u64()), supports_oob) { 337 | (Some(id), true) => 338 | id.try_into().map_err(|e| 339 | io::Error::new(io::ErrorKind::InvalidData, e) 340 | ), 341 | (None, false) => 342 | Ok(Default::default()), 343 | (None, true) => 344 | Err(io::Error::new(io::ErrorKind::InvalidData, format!("QAPI expected response with numeric ID, got {:?}", res.id()))), 345 | (Some(..), false) => 346 | Err(io::Error::new(io::ErrorKind::InvalidData, format!("QAPI expected response without ID, got {:?}", res.id()))), 347 | } 348 | } 349 | 350 | fn handle_response(shared: &QapiShared, res: Response) -> io::Result<()> { 351 | let id = response_id(&res, shared.supports_oob)?; 352 | 353 | if let Some(sender) = shared.command_remove(id) { 354 | sender.send(res.result()).map_err(|_e| 355 | io::Error::new(io::ErrorKind::InvalidData, format!("failed to send response for ID {:?}", id)) 356 | ) 357 | } else { 358 | Err(io::Error::new(io::ErrorKind::InvalidData, format!("unknown QAPI response with ID {:?}", res.id()))) 359 | } 360 | } 361 | 362 | impl Future for QapiEvents where 363 | S: Stream>, 364 | M: TryInto>, 365 | { 366 | type Output = io::Result<()>; 367 | 368 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 369 | let this = unsafe { self.get_unchecked_mut() }; 370 | let stream = unsafe { Pin::new_unchecked(&mut this.stream) }; 371 | let shared = &this.shared; 372 | 373 | shared.poll_next(cx, |cx| Poll::Ready(Some(match futures::ready!(stream.poll_next(cx)) { 374 | None => return Poll::Ready(None), 375 | Some(Err(e)) => Err(e), 376 | Some(Ok(res)) => match res.try_into() { 377 | Ok(res) => match handle_response(shared, res) { 378 | Err(e) => Err(e), 379 | Ok(()) => { 380 | cx.waker().wake_by_ref(); // TODO: I've seen this not work with tokio? 381 | return Poll::Pending 382 | }, 383 | }, 384 | Err(..) => { 385 | trace!("Ignoring QAPI event"); 386 | cx.waker().wake_by_ref(); // TODO: I've seen this not work with tokio? 387 | return Poll::Pending 388 | }, 389 | }, 390 | }))).map(|res| res.unwrap_or(Ok(()))) 391 | } 392 | } 393 | 394 | #[cfg(feature = "qapi-qmp")] 395 | impl>> Stream for QapiEvents { 396 | type Item = io::Result; 397 | 398 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 399 | let this = unsafe { self.get_unchecked_mut() }; 400 | let stream = unsafe { Pin::new_unchecked(&mut this.stream) }; 401 | let shared = &this.shared; 402 | 403 | shared.poll_next(cx, |cx| Poll::Ready(match futures::ready!(stream.poll_next(cx)) { 404 | None => None, // eof 405 | Some(Err(e)) => Some(Err(e)), 406 | Some(Ok(QmpMessage::Event(e))) => Some(Ok(e)), 407 | Some(Ok(QmpMessage::Response(res))) => match handle_response(shared, res) { 408 | Err(e) => Some(Err(e)), 409 | Ok(()) => { 410 | cx.waker().wake_by_ref(); // TODO: I've seen this not work with tokio? 411 | return Poll::Pending 412 | }, 413 | }, 414 | })) 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "autocfg" 31 | version = "1.3.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 34 | 35 | [[package]] 36 | name = "backtrace" 37 | version = "0.3.74" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 40 | dependencies = [ 41 | "addr2line", 42 | "cfg-if", 43 | "libc", 44 | "miniz_oxide", 45 | "object", 46 | "rustc-demangle", 47 | "windows-targets", 48 | ] 49 | 50 | [[package]] 51 | name = "base64" 52 | version = "0.22.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 55 | 56 | [[package]] 57 | name = "bytes" 58 | version = "1.7.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 61 | 62 | [[package]] 63 | name = "cfg-if" 64 | version = "1.0.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 67 | 68 | [[package]] 69 | name = "env_logger" 70 | version = "0.10.2" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 73 | dependencies = [ 74 | "humantime", 75 | "is-terminal", 76 | "log", 77 | "regex", 78 | "termcolor", 79 | ] 80 | 81 | [[package]] 82 | name = "futures" 83 | version = "0.3.30" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 86 | dependencies = [ 87 | "futures-channel", 88 | "futures-core", 89 | "futures-executor", 90 | "futures-io", 91 | "futures-sink", 92 | "futures-task", 93 | "futures-util", 94 | ] 95 | 96 | [[package]] 97 | name = "futures-channel" 98 | version = "0.3.30" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 101 | dependencies = [ 102 | "futures-core", 103 | "futures-sink", 104 | ] 105 | 106 | [[package]] 107 | name = "futures-core" 108 | version = "0.3.30" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 111 | 112 | [[package]] 113 | name = "futures-executor" 114 | version = "0.3.30" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 117 | dependencies = [ 118 | "futures-core", 119 | "futures-task", 120 | "futures-util", 121 | ] 122 | 123 | [[package]] 124 | name = "futures-io" 125 | version = "0.3.30" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 128 | 129 | [[package]] 130 | name = "futures-macro" 131 | version = "0.3.30" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 134 | dependencies = [ 135 | "proc-macro2", 136 | "quote", 137 | "syn", 138 | ] 139 | 140 | [[package]] 141 | name = "futures-sink" 142 | version = "0.3.30" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 145 | 146 | [[package]] 147 | name = "futures-task" 148 | version = "0.3.30" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 151 | 152 | [[package]] 153 | name = "futures-util" 154 | version = "0.3.30" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 157 | dependencies = [ 158 | "futures-channel", 159 | "futures-core", 160 | "futures-io", 161 | "futures-macro", 162 | "futures-sink", 163 | "futures-task", 164 | "memchr", 165 | "pin-project-lite", 166 | "pin-utils", 167 | "slab", 168 | ] 169 | 170 | [[package]] 171 | name = "gimli" 172 | version = "0.31.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" 175 | 176 | [[package]] 177 | name = "hermit-abi" 178 | version = "0.3.9" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 181 | 182 | [[package]] 183 | name = "hermit-abi" 184 | version = "0.4.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 187 | 188 | [[package]] 189 | name = "humantime" 190 | version = "2.1.0" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 193 | 194 | [[package]] 195 | name = "is-terminal" 196 | version = "0.4.13" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" 199 | dependencies = [ 200 | "hermit-abi 0.4.0", 201 | "libc", 202 | "windows-sys 0.52.0", 203 | ] 204 | 205 | [[package]] 206 | name = "itoa" 207 | version = "1.0.11" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 210 | 211 | [[package]] 212 | name = "libc" 213 | version = "0.2.158" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 216 | 217 | [[package]] 218 | name = "log" 219 | version = "0.4.22" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 222 | 223 | [[package]] 224 | name = "memchr" 225 | version = "2.7.4" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 228 | 229 | [[package]] 230 | name = "miniz_oxide" 231 | version = "0.8.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 234 | dependencies = [ 235 | "adler2", 236 | ] 237 | 238 | [[package]] 239 | name = "mio" 240 | version = "1.0.2" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 243 | dependencies = [ 244 | "hermit-abi 0.3.9", 245 | "libc", 246 | "wasi", 247 | "windows-sys 0.52.0", 248 | ] 249 | 250 | [[package]] 251 | name = "object" 252 | version = "0.36.4" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" 255 | dependencies = [ 256 | "memchr", 257 | ] 258 | 259 | [[package]] 260 | name = "pin-project-lite" 261 | version = "0.2.14" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 264 | 265 | [[package]] 266 | name = "pin-utils" 267 | version = "0.1.0" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 270 | 271 | [[package]] 272 | name = "proc-macro2" 273 | version = "1.0.86" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 276 | dependencies = [ 277 | "unicode-ident", 278 | ] 279 | 280 | [[package]] 281 | name = "qapi" 282 | version = "0.15.0" 283 | dependencies = [ 284 | "bytes", 285 | "futures", 286 | "log", 287 | "memchr", 288 | "qapi-qga", 289 | "qapi-qmp", 290 | "qapi-spec", 291 | "serde", 292 | "serde_json", 293 | "tokio", 294 | "tokio-util", 295 | "tower-service", 296 | ] 297 | 298 | [[package]] 299 | name = "qapi-codegen" 300 | version = "0.11.3" 301 | dependencies = [ 302 | "qapi-parser", 303 | ] 304 | 305 | [[package]] 306 | name = "qapi-examples" 307 | version = "0.0.0" 308 | dependencies = [ 309 | "env_logger", 310 | "futures", 311 | "qapi", 312 | "tokio", 313 | ] 314 | 315 | [[package]] 316 | name = "qapi-parser" 317 | version = "0.11.0" 318 | dependencies = [ 319 | "serde", 320 | "serde_json", 321 | ] 322 | 323 | [[package]] 324 | name = "qapi-qga" 325 | version = "0.13.0" 326 | dependencies = [ 327 | "qapi-codegen", 328 | "qapi-spec", 329 | "serde", 330 | ] 331 | 332 | [[package]] 333 | name = "qapi-qmp" 334 | version = "0.15.0" 335 | dependencies = [ 336 | "qapi-codegen", 337 | "qapi-spec", 338 | "serde", 339 | ] 340 | 341 | [[package]] 342 | name = "qapi-spec" 343 | version = "0.3.2" 344 | dependencies = [ 345 | "base64", 346 | "serde", 347 | "serde_json", 348 | ] 349 | 350 | [[package]] 351 | name = "quote" 352 | version = "1.0.37" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 355 | dependencies = [ 356 | "proc-macro2", 357 | ] 358 | 359 | [[package]] 360 | name = "regex" 361 | version = "1.10.6" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 364 | dependencies = [ 365 | "aho-corasick", 366 | "memchr", 367 | "regex-automata", 368 | "regex-syntax", 369 | ] 370 | 371 | [[package]] 372 | name = "regex-automata" 373 | version = "0.4.7" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 376 | dependencies = [ 377 | "aho-corasick", 378 | "memchr", 379 | "regex-syntax", 380 | ] 381 | 382 | [[package]] 383 | name = "regex-syntax" 384 | version = "0.8.4" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 387 | 388 | [[package]] 389 | name = "rustc-demangle" 390 | version = "0.1.24" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 393 | 394 | [[package]] 395 | name = "ryu" 396 | version = "1.0.18" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 399 | 400 | [[package]] 401 | name = "serde" 402 | version = "1.0.210" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 405 | dependencies = [ 406 | "serde_derive", 407 | ] 408 | 409 | [[package]] 410 | name = "serde_derive" 411 | version = "1.0.210" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 414 | dependencies = [ 415 | "proc-macro2", 416 | "quote", 417 | "syn", 418 | ] 419 | 420 | [[package]] 421 | name = "serde_json" 422 | version = "1.0.128" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 425 | dependencies = [ 426 | "itoa", 427 | "memchr", 428 | "ryu", 429 | "serde", 430 | ] 431 | 432 | [[package]] 433 | name = "slab" 434 | version = "0.4.9" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 437 | dependencies = [ 438 | "autocfg", 439 | ] 440 | 441 | [[package]] 442 | name = "socket2" 443 | version = "0.5.7" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 446 | dependencies = [ 447 | "libc", 448 | "windows-sys 0.52.0", 449 | ] 450 | 451 | [[package]] 452 | name = "syn" 453 | version = "2.0.77" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 456 | dependencies = [ 457 | "proc-macro2", 458 | "quote", 459 | "unicode-ident", 460 | ] 461 | 462 | [[package]] 463 | name = "termcolor" 464 | version = "1.4.1" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 467 | dependencies = [ 468 | "winapi-util", 469 | ] 470 | 471 | [[package]] 472 | name = "tokio" 473 | version = "1.40.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" 476 | dependencies = [ 477 | "backtrace", 478 | "bytes", 479 | "libc", 480 | "mio", 481 | "pin-project-lite", 482 | "socket2", 483 | "tokio-macros", 484 | "windows-sys 0.52.0", 485 | ] 486 | 487 | [[package]] 488 | name = "tokio-macros" 489 | version = "2.4.0" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 492 | dependencies = [ 493 | "proc-macro2", 494 | "quote", 495 | "syn", 496 | ] 497 | 498 | [[package]] 499 | name = "tokio-util" 500 | version = "0.7.12" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" 503 | dependencies = [ 504 | "bytes", 505 | "futures-core", 506 | "futures-sink", 507 | "pin-project-lite", 508 | "tokio", 509 | ] 510 | 511 | [[package]] 512 | name = "tower-service" 513 | version = "0.3.3" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 516 | 517 | [[package]] 518 | name = "unicode-ident" 519 | version = "1.0.12" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 522 | 523 | [[package]] 524 | name = "wasi" 525 | version = "0.11.0+wasi-snapshot-preview1" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 528 | 529 | [[package]] 530 | name = "winapi-util" 531 | version = "0.1.9" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 534 | dependencies = [ 535 | "windows-sys 0.59.0", 536 | ] 537 | 538 | [[package]] 539 | name = "windows-sys" 540 | version = "0.52.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 543 | dependencies = [ 544 | "windows-targets", 545 | ] 546 | 547 | [[package]] 548 | name = "windows-sys" 549 | version = "0.59.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 552 | dependencies = [ 553 | "windows-targets", 554 | ] 555 | 556 | [[package]] 557 | name = "windows-targets" 558 | version = "0.52.6" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 561 | dependencies = [ 562 | "windows_aarch64_gnullvm", 563 | "windows_aarch64_msvc", 564 | "windows_i686_gnu", 565 | "windows_i686_gnullvm", 566 | "windows_i686_msvc", 567 | "windows_x86_64_gnu", 568 | "windows_x86_64_gnullvm", 569 | "windows_x86_64_msvc", 570 | ] 571 | 572 | [[package]] 573 | name = "windows_aarch64_gnullvm" 574 | version = "0.52.6" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 577 | 578 | [[package]] 579 | name = "windows_aarch64_msvc" 580 | version = "0.52.6" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 583 | 584 | [[package]] 585 | name = "windows_i686_gnu" 586 | version = "0.52.6" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 589 | 590 | [[package]] 591 | name = "windows_i686_gnullvm" 592 | version = "0.52.6" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 595 | 596 | [[package]] 597 | name = "windows_i686_msvc" 598 | version = "0.52.6" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 601 | 602 | [[package]] 603 | name = "windows_x86_64_gnu" 604 | version = "0.52.6" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 607 | 608 | [[package]] 609 | name = "windows_x86_64_gnullvm" 610 | version = "0.52.6" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 613 | 614 | [[package]] 615 | name = "windows_x86_64_msvc" 616 | version = "0.52.6" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 619 | -------------------------------------------------------------------------------- /parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_root_url = "https://docs.rs/qapi-parser/0.11.0")] 2 | 3 | pub mod spec { 4 | use std::fmt; 5 | use serde::de::{Deserializer, Visitor, SeqAccess, MapAccess, Error}; 6 | use serde::de::value::MapAccessDeserializer; 7 | use serde::Deserialize; 8 | 9 | #[derive(Debug, Clone, Deserialize)] 10 | #[serde(untagged, rename_all = "lowercase")] 11 | pub enum Spec { 12 | Include(Include), 13 | Command(Command), 14 | Struct(Struct), 15 | Alternate(Alternate), 16 | Enum(Enum), 17 | Event(Event), 18 | CombinedUnion(CombinedUnion), 19 | Union(Union), 20 | PragmaWhitelist { 21 | pragma: PragmaWhitelist 22 | }, 23 | PragmaExceptions { 24 | pragma: PragmaExceptions 25 | }, 26 | PragmaDocRequired { 27 | pragma: PragmaDocRequired 28 | }, 29 | } 30 | 31 | #[derive(Debug, Default, Clone)] 32 | pub struct Data { 33 | pub fields: Vec, 34 | } 35 | 36 | impl Data { 37 | pub fn is_empty(&self) -> bool { 38 | self.fields.is_empty() || self.fields.iter().all(|f| f.optional) 39 | } 40 | 41 | pub fn newtype(&self) -> Option<&Value> { 42 | match self.fields.get(0) { 43 | Some(data) if self.fields.len() == 1 => Some(data), 44 | _ => None, 45 | } 46 | } 47 | } 48 | 49 | impl<'de> Deserialize<'de> for Data { 50 | fn deserialize>(d: D) -> Result { 51 | use serde::de::IntoDeserializer; 52 | 53 | let fields: serde_json::Map = Deserialize::deserialize(d)?; 54 | let fields = fields.into_iter().map(|(n, t)| 55 | Type::deserialize(t.into_deserializer()).map(|t| 56 | Value::new(&n, t) 57 | ).map_err(D::Error::custom) 58 | ).collect::>()?; 59 | 60 | Ok(Data { 61 | fields, 62 | }) 63 | } 64 | } 65 | 66 | #[derive(Debug, Clone)] 67 | pub struct Value { 68 | pub name: String, 69 | pub ty: Type, 70 | pub optional: bool, 71 | } 72 | 73 | impl Value { 74 | pub fn new(name: &str, ty: Type) -> Self { 75 | let (name, opt) = if name.starts_with("*") { 76 | (name[1..].into(), true) 77 | } else { 78 | (name.into(), false) 79 | }; 80 | 81 | Value { 82 | name, 83 | ty, 84 | optional: opt, 85 | } 86 | } 87 | } 88 | 89 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] 90 | #[serde(rename_all = "kebab-case")] 91 | pub enum Feature { 92 | Deprecated, 93 | Unstable, 94 | JsonCli, 95 | JsonCliHotplug, 96 | // what are these? 97 | AllowWriteOnlyOverlay, 98 | DynamicAutoReadOnly, 99 | SavevmMonitorNodes, 100 | Fdset, 101 | } 102 | 103 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] 104 | #[serde(untagged)] 105 | pub enum ConditionalFeature { 106 | Feature(Feature), 107 | Conditional { 108 | name: Feature, 109 | #[serde(default)] 110 | conditional: Option, 111 | }, 112 | } 113 | 114 | impl PartialEq for ConditionalFeature { 115 | fn eq(&self, rhs: &Feature) -> bool { 116 | match self { 117 | ConditionalFeature::Feature(name) => name == rhs, 118 | ConditionalFeature::Conditional { name, .. } => name == rhs, 119 | } 120 | } 121 | } 122 | 123 | #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] 124 | #[serde(transparent)] 125 | pub struct Features { 126 | // TODO: make this a set instead? 127 | pub features: Vec, 128 | } 129 | 130 | impl Features { 131 | pub fn is_deprecated(&self) -> bool { 132 | self.features.iter().any(|f| f == &Feature::Deprecated) 133 | } 134 | } 135 | 136 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 137 | pub struct Type { 138 | pub name: String, 139 | pub is_array: bool, 140 | pub conditional: Option, 141 | pub features: Features, 142 | } 143 | 144 | impl fmt::Debug for Type { 145 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 146 | if self.is_array { 147 | write!(fmt, "[{}]", self.name)? 148 | } else { 149 | write!(fmt, "{}", self.name)? 150 | } 151 | 152 | if let Some(cond) = &self.conditional { 153 | write!(fmt, " {:?}", cond) 154 | } else { 155 | Ok(()) 156 | } 157 | } 158 | } 159 | 160 | impl<'de> Deserialize<'de> for Type { 161 | fn deserialize>(d: D) -> Result { 162 | struct V; 163 | 164 | impl<'de> Visitor<'de> for V { 165 | type Value = Type; 166 | 167 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 168 | write!(formatter, "a Type of string or single element array") 169 | } 170 | 171 | fn visit_str(self, v: &str) -> Result { 172 | Ok(Type { 173 | name: v.into(), 174 | is_array: false, 175 | conditional: None, 176 | features: Default::default(), 177 | }) 178 | } 179 | 180 | fn visit_map>(self, map: A) -> Result { 181 | #[derive(Debug, Clone, Deserialize)] 182 | struct ConditionalType { 183 | #[serde(rename = "type")] 184 | ty: Type, 185 | #[serde(default, rename = "if")] 186 | conditional: Option, 187 | #[serde(default)] 188 | features: Features, 189 | } 190 | 191 | let ty = ConditionalType::deserialize(MapAccessDeserializer::new(map))?; 192 | Ok(Type { 193 | conditional: ty.conditional, 194 | features: ty.features, 195 | .. ty.ty 196 | }) 197 | } 198 | 199 | fn visit_string(self, v: String) -> Result { 200 | Ok(Type { 201 | name: v, 202 | is_array: false, 203 | conditional: None, 204 | features: Default::default(), 205 | }) 206 | } 207 | 208 | fn visit_seq>(self, mut seq: A) -> Result { 209 | let v = seq.next_element::()?; 210 | if let Some(v) = v { 211 | if seq.next_element::()?.is_none() { 212 | Ok(Type { 213 | name: v, 214 | is_array: true, 215 | conditional: None, 216 | features: Default::default(), 217 | }) 218 | } else { 219 | Err(A::Error::invalid_length(2, &"single array item")) 220 | } 221 | } else { 222 | Err(A::Error::invalid_length(0, &"single array item")) 223 | } 224 | } 225 | } 226 | 227 | d.deserialize_any(V) 228 | } 229 | } 230 | 231 | /// A #define'd symbol such as `CONFIG_SPICE` 232 | pub type ConditionalDefinition = String; 233 | 234 | #[derive(Debug, Clone, Deserialize, PartialOrd, Ord, PartialEq, Eq, Hash)] 235 | #[serde(untagged, rename_all = "kebab-case")] 236 | pub enum Conditional { 237 | Define(ConditionalDefinition), 238 | Not { 239 | not: ConditionalDefinition, 240 | }, 241 | All { 242 | all: Vec, 243 | }, 244 | Any { 245 | any: Vec, 246 | }, 247 | } 248 | 249 | #[derive(Debug, Clone, Deserialize)] 250 | #[serde(rename_all = "kebab-case")] 251 | pub struct Include { 252 | pub include: String, 253 | } 254 | 255 | #[derive(Debug, Clone, Deserialize)] 256 | #[serde(rename_all = "kebab-case")] 257 | pub struct Command { 258 | #[serde(rename = "command")] 259 | pub id: String, 260 | #[serde(default)] 261 | pub data: DataOrType, 262 | #[serde(default)] 263 | pub returns: Option, 264 | #[serde(default, rename = "if")] 265 | pub conditional: Option, 266 | #[serde(default)] 267 | pub allow_oob: bool, 268 | #[serde(default)] 269 | pub coroutine: bool, 270 | #[serde(default)] 271 | pub boxed: bool, 272 | #[serde(default = "Command::success_response_default")] 273 | pub success_response: bool, 274 | #[serde(default)] 275 | pub allow_preconfig: bool, 276 | #[serde(default)] 277 | pub features: Features, 278 | #[serde(default = "Command::gen_default")] 279 | pub gen: bool, 280 | } 281 | 282 | impl Command { 283 | fn success_response_default() -> bool { true } 284 | fn gen_default() -> bool { true } 285 | } 286 | 287 | #[derive(Debug, Clone, Deserialize)] 288 | #[serde(rename_all = "kebab-case")] 289 | pub struct Struct { 290 | #[serde(rename = "struct")] 291 | pub id: String, 292 | #[serde(default)] 293 | pub data: Data, 294 | #[serde(default)] 295 | pub base: DataOrType, 296 | #[serde(default, rename = "if")] 297 | pub conditional: Option, 298 | #[serde(default)] 299 | pub features: Features, 300 | } 301 | 302 | impl Struct { 303 | pub fn newtype(&self) -> Option<&Value> { 304 | match &self.base { 305 | DataOrType::Data(d) if d.fields.is_empty() => (), 306 | _ => return None, 307 | } 308 | self.data.newtype() 309 | } 310 | 311 | pub fn wrapper_type(&self) -> Option<&Value> { 312 | match self.id.ends_with("Wrapper") { 313 | true => self.newtype(), 314 | false => None, 315 | } 316 | } 317 | 318 | pub fn is_empty(&self) -> bool { 319 | self.base.is_empty() && self.data.is_empty() 320 | } 321 | } 322 | 323 | #[derive(Debug, Clone, Deserialize)] 324 | #[serde(rename_all = "kebab-case")] 325 | pub struct Alternate { 326 | #[serde(rename = "alternate")] 327 | pub id: String, 328 | #[serde(default)] 329 | pub data: Data, 330 | #[serde(default, rename = "if")] 331 | pub conditional: Option, 332 | } 333 | 334 | #[derive(Debug, Clone, Deserialize)] 335 | #[serde(rename_all = "kebab-case")] 336 | pub struct Enum { 337 | #[serde(rename = "enum")] 338 | pub id: String, 339 | #[serde(default)] 340 | pub data: Vec, 341 | #[serde(default)] 342 | pub prefix: Option, 343 | #[serde(default, rename = "if")] 344 | pub conditional: Option, 345 | } 346 | 347 | #[derive(Debug, Clone, Deserialize)] 348 | #[serde(rename_all = "kebab-case")] 349 | pub struct CombinedUnion { 350 | #[serde(rename = "union")] 351 | pub id: String, 352 | pub base: DataOrType, 353 | #[serde(default)] 354 | pub discriminator: Option, 355 | pub data: Data, 356 | #[serde(default, rename = "if")] 357 | pub conditional: Option, 358 | } 359 | 360 | #[derive(Debug, Clone, Deserialize)] 361 | #[serde(rename_all = "kebab-case")] 362 | pub struct Union { 363 | #[serde(rename = "union")] 364 | pub id: String, 365 | #[serde(default)] 366 | pub discriminator: Option, 367 | pub data: Data, 368 | #[serde(default, rename = "if")] 369 | pub conditional: Option, 370 | } 371 | 372 | #[derive(Debug, Clone, Deserialize)] 373 | #[serde(untagged, rename_all = "kebab-case")] 374 | pub enum DataOrType { 375 | Data(Data), 376 | Type(Type), 377 | } 378 | 379 | impl Default for DataOrType { 380 | fn default() -> Self { 381 | DataOrType::Data(Default::default()) 382 | } 383 | } 384 | 385 | impl DataOrType { 386 | pub fn is_empty(&self) -> bool { 387 | match self { 388 | &DataOrType::Data(ref data) => data.fields.is_empty(), 389 | &DataOrType::Type(..) => false, 390 | } 391 | } 392 | 393 | pub fn len(&self) -> usize { 394 | match self { 395 | &DataOrType::Data(ref data) => data.fields.len(), 396 | &DataOrType::Type(..) => 1, 397 | } 398 | } 399 | } 400 | 401 | #[derive(Debug, Clone, Deserialize)] 402 | #[serde(rename_all = "kebab-case")] 403 | pub struct Event { 404 | #[serde(rename = "event")] 405 | pub id: String, 406 | #[serde(default)] 407 | pub data: DataOrType, 408 | #[serde(default, rename = "if")] 409 | pub conditional: Option, 410 | #[serde(default)] 411 | pub features: Features, 412 | } 413 | 414 | #[derive(Debug, Clone, Deserialize)] 415 | #[serde(rename_all = "kebab-case")] 416 | pub struct PragmaWhitelist { 417 | pub returns_whitelist: Vec, 418 | #[serde(default)] 419 | pub name_case_whitelist: Vec, 420 | } 421 | 422 | #[derive(Debug, Clone, Deserialize)] 423 | #[serde(rename_all = "kebab-case")] 424 | pub struct PragmaExceptions { 425 | pub command_returns_exceptions: Vec, 426 | #[serde(default)] 427 | pub member_name_exceptions: Vec, 428 | } 429 | 430 | #[derive(Debug, Clone, Deserialize)] 431 | #[serde(rename_all = "kebab-case")] 432 | pub struct PragmaDocRequired { 433 | pub doc_required: bool, 434 | } 435 | 436 | #[derive(Debug, Clone, Deserialize)] 437 | #[serde(untagged, rename_all = "kebab-case")] 438 | pub enum SpecName { 439 | Name(String), 440 | Conditional { 441 | name: String, 442 | #[serde(rename = "if")] 443 | conditional: Conditional, 444 | #[serde(default)] 445 | features: Features, 446 | }, 447 | Explicit { 448 | name: String, 449 | #[serde(default)] 450 | features: Features, 451 | }, 452 | } 453 | 454 | impl SpecName { 455 | pub fn name(&self) -> &String { 456 | match self { 457 | SpecName::Name(name) | SpecName::Explicit { name, .. } => name, 458 | SpecName::Conditional { name, .. } => name, 459 | } 460 | } 461 | 462 | pub fn features(&self) -> Option<&Features> { 463 | match self { 464 | SpecName::Name(..) => None, 465 | SpecName::Explicit { features, .. } => Some(features), 466 | SpecName::Conditional { features, .. } => Some(features), 467 | } 468 | } 469 | } 470 | 471 | impl fmt::Display for SpecName { 472 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 473 | fmt::Display::fmt(self.as_ref(), fmt) 474 | } 475 | } 476 | impl AsRef for SpecName { 477 | fn as_ref(&self) -> &str { 478 | &self.name()[..] 479 | } 480 | } 481 | } 482 | 483 | pub use self::spec::Spec; 484 | 485 | use std::path::{Path, PathBuf}; 486 | use std::ops::{Deref, DerefMut}; 487 | use std::io; 488 | 489 | pub struct Parser { 490 | data: String, 491 | pos: usize, 492 | eof: bool, 493 | } 494 | 495 | impl Parser { 496 | pub fn from_string>(s: S) -> Self { 497 | Parser { 498 | data: s.into(), 499 | pos: 0, 500 | eof: false, 501 | } 502 | } 503 | 504 | pub fn strip_comments(s: &str) -> String { 505 | let lines: Vec = s.lines() 506 | .filter(|l| !l.trim().starts_with("#") && !l.trim().is_empty()) 507 | .map(|s| s.replace("'", "\"")) 508 | .map(|s| if let Some(i) = s.find('#') { 509 | s[..i].to_owned() 510 | } else { 511 | s 512 | }).collect(); 513 | lines.join("\n") 514 | } 515 | } 516 | 517 | impl Iterator for Parser { 518 | type Item = serde_json::Result; 519 | 520 | fn next(&mut self) -> Option { 521 | if self.eof { 522 | None 523 | } else { 524 | Some(match serde_json::from_str(&self.data[self.pos..]) { 525 | Ok(res) => { 526 | self.eof = true; 527 | Ok(res) 528 | }, 529 | Err(e) => { 530 | let (line, col) = (e.line(), e.column()); 531 | if line == 0 || col == 0 { 532 | Err(e) 533 | } else { 534 | let count: usize = self.data[self.pos..].lines().map(|l| l.len() + 1).take(line - 1).sum(); 535 | let str = &self.data[self.pos .. (self.pos + count + col - 1)]; 536 | self.pos += count; 537 | serde_json::from_str(str) 538 | } 539 | }, 540 | }) 541 | } 542 | } 543 | } 544 | 545 | pub trait QemuRepo { 546 | type Error; 547 | 548 | fn push_context>(&mut self, p: P); 549 | fn pop_context(&mut self); 550 | fn context(&self) -> &Path; 551 | 552 | fn include>(&mut self, p: P) -> Result<(QemuRepoContext<'_, Self>, String), Self::Error>; 553 | } 554 | 555 | #[derive(Debug, Clone)] 556 | pub struct QemuFileRepo { 557 | paths: Vec, 558 | } 559 | 560 | pub struct QemuRepoContext<'a, R: QemuRepo + ?Sized + 'a> { 561 | repo: &'a mut R, 562 | } 563 | 564 | impl<'a, R: QemuRepo + ?Sized + 'a> QemuRepoContext<'a, R> { 565 | pub fn from_include>(repo: &'a mut R, path: P) -> (Self, PathBuf) { 566 | let path = path.as_ref(); 567 | let include_path = repo.context().join(path); 568 | repo.push_context(include_path.parent().unwrap()); 569 | 570 | ( 571 | QemuRepoContext { 572 | repo, 573 | }, 574 | include_path, 575 | ) 576 | } 577 | } 578 | 579 | impl<'a, R: QemuRepo + ?Sized + 'a> Deref for QemuRepoContext<'a, R> { 580 | type Target = R; 581 | 582 | fn deref(&self) -> &Self::Target { 583 | self.repo 584 | } 585 | } 586 | 587 | impl<'a, R: QemuRepo + ?Sized + 'a> DerefMut for QemuRepoContext<'a, R> { 588 | fn deref_mut(&mut self) -> &mut Self::Target { 589 | self.repo 590 | } 591 | } 592 | 593 | impl<'a, R: QemuRepo + ?Sized + 'a> Drop for QemuRepoContext<'a, R> { 594 | fn drop(&mut self) { 595 | self.repo.pop_context(); 596 | } 597 | } 598 | 599 | impl QemuFileRepo { 600 | pub fn new>(p: P) -> Self { 601 | QemuFileRepo { 602 | paths: vec![p.into()], 603 | } 604 | } 605 | } 606 | 607 | impl QemuRepo for QemuFileRepo { 608 | type Error = io::Error; 609 | 610 | fn push_context>(&mut self, p: P) { 611 | self.paths.push(p.as_ref().to_owned()); 612 | } 613 | 614 | fn pop_context(&mut self) { 615 | self.paths.pop(); 616 | assert!(!self.paths.is_empty()); 617 | } 618 | 619 | fn context(&self) -> &Path { 620 | self.paths.last().unwrap() 621 | } 622 | 623 | fn include>(&mut self, p: P) -> Result<(QemuRepoContext<'_, Self>, String), Self::Error> { 624 | use std::fs::File; 625 | use std::io::Read; 626 | 627 | let (context, path) = QemuRepoContext::from_include(self, p); 628 | let mut f = File::open(path)?; 629 | let mut str = String::new(); 630 | f.read_to_string(&mut str)?; 631 | Ok((context, str)) 632 | } 633 | } 634 | 635 | #[cfg(test)] 636 | mod test { 637 | use super::*; 638 | use std::path::Path; 639 | 640 | fn parse_include>(repo: &mut QemuFileRepo, include: P) { 641 | let include = include.as_ref(); 642 | println!("including {}", include.display()); 643 | 644 | let (mut context, schema) = repo.include(include).expect("include path not found"); 645 | for item in Parser::from_string(Parser::strip_comments(&schema)) { 646 | match item.expect("schema parse failure") { 647 | Spec::Include(inc) => parse_include(&mut context, inc.include), 648 | item => println!("decoded {:?}", item), 649 | } 650 | } 651 | } 652 | 653 | fn parse_schema(mut repo: QemuFileRepo) { 654 | parse_include(&mut repo, "qapi-schema.json"); 655 | } 656 | 657 | #[test] 658 | fn parse_qapi() { 659 | parse_schema(QemuFileRepo::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../schema/qapi/"))); 660 | } 661 | 662 | #[test] 663 | fn parse_qga() { 664 | parse_schema(QemuFileRepo::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../schema/qga/"))); 665 | } 666 | } 667 | -------------------------------------------------------------------------------- /codegen/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_root_url = "https://docs.rs/qapi-codegen/0.11.3")] 2 | 3 | //! Generates Rust types for the [QAPI schema language](https://qemu-project.gitlab.io/qemu/devel/qapi-code-gen.html#the-qapi-schema-language) 4 | 5 | use qapi_parser::{Parser, QemuFileRepo, QemuRepo, spec}; 6 | use qapi_parser::spec::Spec; 7 | use std::collections::{BTreeMap, HashSet}; 8 | use std::path::{Path, PathBuf}; 9 | use std::fs::File; 10 | use std::io::{self, Write}; 11 | use std::mem::replace; 12 | 13 | // kebab-case to PascalCase? 14 | fn type_identifier>(id: S) -> String { 15 | identifier(id) 16 | } 17 | 18 | // kebab-case to snake_case 19 | fn identifier>(id: S) -> String { 20 | let id = id.as_ref(); 21 | match id { 22 | "type" | "static" | "virtual" | "abstract" | "in" | "if" | "enum" | "match" | "use" => format!("{}_", id), 23 | s if s.as_bytes()[0].is_ascii_digit() => format!("_{}", s), 24 | id => id.replace("-", "_") 25 | } 26 | } 27 | 28 | // SCREAMING_SNAKE_CASE to PascalCase? 29 | fn event_identifier(id: &str) -> String { 30 | id.into() 31 | } 32 | 33 | // no case change, just check for rust primitives 34 | fn typename_s(ty: &str) -> String { 35 | match ty { 36 | "str" => "::std::string::String".into(), 37 | "any" => "::qapi_spec::Any".into(), 38 | "null" => "()".into(), 39 | "number" => "f64".into(), 40 | "int8" => "i8".into(), 41 | "uint8" => "u8".into(), 42 | "int16" => "i16".into(), 43 | "uint16" => "u16".into(), 44 | "int32" => "i32".into(), 45 | "uint32" => "u32".into(), 46 | "int64" => "i64".into(), 47 | "uint64" => "u64".into(), 48 | "size" => "u64".into(), 49 | "int" => "i64".into(), 50 | ty => ty.into(), 51 | } 52 | } 53 | 54 | fn type_attrs(ty: &spec::Type) -> String { 55 | feature_attrs(&ty.features) 56 | } 57 | 58 | fn feature_attrs(ty: &spec::Features) -> String { 59 | if ty.is_deprecated() { " #[deprecated]".into() } else { String::new() } 60 | } 61 | 62 | fn typename(ty: &spec::Type) -> String { 63 | if ty.is_array { 64 | format!("Vec<{}>", typename_s(&ty.name)) 65 | } else { 66 | typename_s(&ty.name) 67 | } 68 | } 69 | 70 | fn valuety(value: &spec::Value, pubvis: bool, super_name: &str) -> String { 71 | // overrides for recursive types: 72 | let boxed = value.ty.name == super_name; 73 | 74 | let base64 = value.ty.name == "str" && ( 75 | ((super_name == "GuestFileRead" || super_name == "guest-file-write") && value.name == "buf-b64") || 76 | (super_name == "guest-set-user-password" && value.name == "password") || 77 | (super_name == "GuestExecStatus" && (value.name == "out-data" || value.name == "err-data")) || 78 | (super_name == "guest-exec" && value.name == "input-data") || 79 | (super_name == "get-win32-socket" && value.name == "info") || 80 | (super_name == "SevGuestProperties" && (value.name == "dh-cert-file" || value.name == "session-file")) || 81 | (super_name == "SecretCommonProperties" && value.name == "iv") 82 | // `ringbuf-write`, `ringbuf-read`, `SecretProperties` can't be done because weird enums 83 | ); 84 | 85 | let dict = value.ty.name == "any" && ( 86 | (super_name == "object-add" && value.name == "props") || 87 | (super_name == "CpuModelInfo" && value.name == "props") 88 | ); 89 | 90 | // TODO: handle optional Vec<>s specially? 91 | 92 | let ty = typename(&value.ty); 93 | let (attr, ty) = if base64 { 94 | let ty = "Vec".into(); 95 | if value.optional { 96 | (", with = \"::qapi_spec::base64_opt\"", ty) 97 | } else { 98 | (", with = \"::qapi_spec::base64\"", ty) 99 | } 100 | } else if boxed { 101 | ("", format!("Box<{}>", ty)) 102 | } else if dict { 103 | ("", "::qapi_spec::Dictionary".into()) 104 | } else if super_name == "guest-shutdown" && value.name == "mode" { 105 | ("", "GuestShutdownMode".into()) 106 | } else { 107 | ("", ty) 108 | }; 109 | 110 | let (attr, ty) = if value.optional { 111 | (format!("{}, default, skip_serializing_if = \"Option::is_none\"", attr), format!("Option<{}>", ty)) 112 | } else { 113 | (attr.into(), ty) 114 | }; 115 | 116 | format!("#[serde(rename = \"{}\"{})]{}\n{}{}: {}", 117 | value.name, 118 | attr, 119 | type_attrs(&value.ty), 120 | if pubvis { "pub " } else { "" }, 121 | identifier(&value.name), 122 | ty 123 | ) 124 | } 125 | 126 | struct Context { 127 | includes: Vec, 128 | included: HashSet, 129 | events: Vec, 130 | unions: BTreeMap, 131 | enums: BTreeMap, 132 | types: BTreeMap, 133 | struct_discriminators: BTreeMap, 134 | command_trait: String, 135 | out: W, 136 | } 137 | 138 | impl Context { 139 | fn new(out: W, command_trait: String) -> Self { 140 | Context { 141 | includes: Default::default(), 142 | included: Default::default(), 143 | events: Default::default(), 144 | unions: Default::default(), 145 | enums: Default::default(), 146 | types: Default::default(), 147 | struct_discriminators: Default::default(), 148 | command_trait, 149 | out, 150 | } 151 | } 152 | 153 | fn process(&mut self, item: spec::Spec) -> io::Result<()> { 154 | match item { 155 | Spec::Include(include) => { 156 | self.includes.push(include.include); 157 | }, 158 | Spec::Command(v) => { 159 | let type_id = type_identifier(&v.id); 160 | match v.data { 161 | spec::DataOrType::Type(ref ty) if type_identifier(&ty.name) == type_id => (), 162 | ty => { 163 | write!(self.out, " 164 | #[derive(Debug, Clone, Serialize, Deserialize)]{} 165 | pub struct {}", feature_attrs(&v.features), type_id)?; 166 | match ty { 167 | spec::DataOrType::Data(ref data) => { 168 | writeln!(self.out, " {{")?; 169 | for data in &data.fields { 170 | writeln!(self.out, "\t{},", valuety(&data, true, &v.id))?; 171 | } 172 | if !v.gen { 173 | writeln!(self.out, " 174 | #[serde(flatten)] 175 | pub arguments: ::qapi_spec::Dictionary, 176 | ")?; 177 | } 178 | writeln!(self.out, "}}")?; 179 | }, 180 | spec::DataOrType::Type(ref ty) => { 181 | let ty_name = type_identifier(&ty.name); 182 | writeln!(self.out, "({}pub {});", type_attrs(ty), ty_name)?; 183 | writeln!(self.out, " 184 | impl From<{}> for {} {{ 185 | fn from(val: {}) -> Self {{ 186 | Self(val) 187 | }} 188 | }} 189 | ", ty_name, type_id, ty_name)?; 190 | }, 191 | } 192 | }, 193 | } 194 | 195 | write!(self.out, " 196 | impl crate::{} for {} {{ }} 197 | impl ::qapi_spec::Command for {} {{ 198 | const NAME: &'static str = \"{}\"; 199 | const ALLOW_OOB: bool = {}; 200 | 201 | type Ok = ", self.command_trait, type_id, type_id, v.id, v.allow_oob)?; 202 | if let Some(ret) = v.returns { 203 | writeln!(self.out, "{};", typename(&ret)) 204 | } else { 205 | writeln!(self.out, "::qapi_spec::Empty;") 206 | }?; 207 | writeln!(self.out, "}}")?; 208 | }, 209 | Spec::Struct(v) => { 210 | self.types.insert(v.id.clone(), v); 211 | }, 212 | Spec::Alternate(v) => { 213 | write!(self.out, " 214 | #[derive(Debug, Clone, Serialize, Deserialize)] 215 | #[serde(untagged)] 216 | pub enum {} {{ 217 | ", type_identifier(&v.id))?; 218 | for data in &v.data.fields { 219 | assert!(!data.optional); 220 | let boxed = if data.name == "definition" && data.ty.name == "BlockdevOptions" { 221 | true 222 | } else { 223 | false 224 | }; 225 | let ty = if boxed { 226 | format!("Box<{}>", typename(&data.ty)) 227 | } else { 228 | typename(&data.ty) 229 | }; 230 | writeln!(self.out, "\t#[serde(rename = \"{}\")] {}({}),", data.name, type_identifier(&data.name), ty)?; 231 | } 232 | writeln!(self.out, "}}")?; 233 | }, 234 | Spec::Enum(v) => { 235 | let type_id = type_identifier(&v.id); 236 | write!(self.out, " 237 | #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] 238 | pub enum {} {{ 239 | ", type_id)?; 240 | for item in &v.data { 241 | writeln!(self.out, "\t#[serde(rename = \"{}\")] {},", item, type_identifier(item))?; 242 | } 243 | writeln!(self.out, "}}")?; 244 | writeln!(self.out, " 245 | impl ::core::str::FromStr for {} {{ 246 | type Err = (); 247 | 248 | fn from_str(s: &str) -> Result {{ 249 | ::qapi_spec::Enum::from_name(s).ok_or(()) 250 | }} 251 | }} 252 | 253 | unsafe impl ::qapi_spec::Enum for {} {{ 254 | fn discriminant(&self) -> usize {{ *self as usize }} 255 | 256 | const COUNT: usize = {}; 257 | const VARIANTS: &'static [Self] = &[ 258 | ", type_id, type_id, v.data.len())?; 259 | for item in &v.data { 260 | writeln!(self.out, "{}::{},", type_id, type_identifier(item))?; 261 | } 262 | writeln!(self.out, " 263 | ]; 264 | const NAMES: &'static [&'static str] = &[ 265 | ")?; 266 | for item in &v.data { 267 | writeln!(self.out, "\"{}\",", item)?; 268 | } 269 | writeln!(self.out, " 270 | ]; 271 | }}")?; 272 | self.enums.insert(v.id.clone(), v); 273 | }, 274 | Spec::Event(v) => { 275 | write!(self.out, " 276 | #[derive(Debug, Clone, Serialize, Deserialize{})] 277 | ", if v.data.is_empty() { ", Default" } else { "" })?; 278 | if let spec::DataOrType::Type(..) = v.data { 279 | writeln!(self.out, "#[serde(transparent)]")?; 280 | writeln!(self.out, "#[repr(transparent)]")?; 281 | } 282 | writeln!(self.out, "pub struct {} {{", event_identifier(&v.id))?; 283 | match v.data { 284 | spec::DataOrType::Type(ref ty) => { 285 | let data = spec::Value { 286 | name: "data".into(), 287 | ty: ty.clone(), 288 | optional: false, 289 | }; 290 | writeln!(self.out, "{},", valuety(&data, true, &v.id))? 291 | }, 292 | spec::DataOrType::Data(ref data) => for item in &data.fields { 293 | writeln!(self.out, "{},", valuety(item, true, &v.id))?; 294 | }, 295 | } 296 | writeln!(self.out, "}}")?; 297 | writeln!(self.out, " 298 | impl ::qapi_spec::Event for {} {{ 299 | const NAME: &'static str = \"{}\"; 300 | }}", event_identifier(&v.id), v.id)?; 301 | self.events.push(v); 302 | }, 303 | Spec::Union(v) => { 304 | write!(self.out, " 305 | #[derive(Debug, Clone, Serialize, Deserialize)] 306 | #[serde(tag = \"{}\")] 307 | pub enum {} {{ 308 | ", if let Some(ref tag) = v.discriminator { tag } else { "type" }, type_identifier(&v.id))?; 309 | for data in &v.data.fields { 310 | writeln!(self.out, "\t#[serde(rename = \"{}\")]\n\t{} {{ data: {} }},", data.name, type_identifier(&data.name), typename(&data.ty))?; 311 | } 312 | writeln!(self.out, "}}")?; 313 | }, 314 | Spec::CombinedUnion(v) => { 315 | self.unions.insert(v.id.clone(), v); 316 | }, 317 | Spec::PragmaWhitelist { .. } => (), 318 | Spec::PragmaExceptions { .. } => (), 319 | Spec::PragmaDocRequired { .. } => (), 320 | } 321 | 322 | Ok(()) 323 | } 324 | 325 | fn process_structs(&mut self) -> io::Result<()> { 326 | for (id, discrim) in &self.struct_discriminators { 327 | let ty = self.types.get_mut(id).ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("could not find qapi type {}", id)))?; 328 | let fields = replace(&mut ty.data.fields, Vec::new()); 329 | ty.data.fields = fields.into_iter().filter(|base| &base.name != discrim).collect(); 330 | } 331 | 332 | for v in self.types.values() { 333 | let struct_id = type_identifier(&v.id); 334 | write!(self.out, " 335 | #[derive(Debug, Clone, Serialize, Deserialize{})]{}{} 336 | pub struct {} {{ 337 | ", if v.is_empty() { ", Default" } else { "" }, if v.wrapper_type().is_some() { "#[repr(transparent)]" } else { "" }, feature_attrs(&v.features), struct_id)?; 338 | match v.base { 339 | spec::DataOrType::Data(ref data) => for base in &data.fields { 340 | writeln!(self.out, "{},", valuety(base, true, &v.id))?; 341 | }, 342 | spec::DataOrType::Type(ref ty) => { 343 | let base = spec::Value { 344 | name: "base".into(), 345 | ty: ty.clone(), 346 | optional: false, 347 | }; 348 | writeln!(self.out, "#[serde(flatten)]\n{},", valuety(&base, true, &v.id))?; 349 | }, 350 | } 351 | for item in &v.data.fields { 352 | writeln!(self.out, "{},", valuety(item, true, &v.id))?; 353 | } 354 | writeln!(self.out, "}}")?; 355 | 356 | let basetype = match v.data.is_empty() { 357 | true => match v.base { 358 | spec::DataOrType::Type(ref ty) => Some(spec::Value::new("base", ty.clone())), 359 | spec::DataOrType::Data(ref data) => data.newtype().cloned(), 360 | }, 361 | false => None, 362 | }; 363 | let newtype = v.newtype(); 364 | let wrapper = v.wrapper_type(); 365 | if let Some(field) = v.newtype().or(basetype.as_ref()) { 366 | let field_ty = typename(&field.ty); 367 | let field_name = identifier(&field.name); 368 | let into = if field.optional { ".into()" } else { "" }; 369 | write!(self.out, " 370 | impl> From for {} {{ 371 | fn from(val: T) -> Self {{ 372 | Self {{ 373 | {}: val.into(){}, 374 | ", field_ty, struct_id, field_name, into)?; 375 | if newtype.is_none() { 376 | for field in &v.data.fields { 377 | writeln!(self.out, "{}: Default::default(),", identifier(&field.name))?; 378 | } 379 | } 380 | write!(self.out, " 381 | }} 382 | }} 383 | }}")?; 384 | if !field.optional { 385 | write!(self.out, " 386 | impl AsRef<{}> for {} {{ 387 | fn as_ref(&self) -> &{} {{ 388 | &self.{} 389 | }} 390 | }}", field_ty, struct_id, field_ty, field_name)?; 391 | } 392 | } 393 | if let Some(field) = wrapper { 394 | let field_ty = typename(&field.ty); 395 | let field_name = identifier(&field.name); 396 | write!(self.out, " 397 | impl ::std::ops::Deref for {} {{ 398 | type Target = {}; 399 | 400 | fn deref(&self) -> &Self::Target {{ 401 | &self.{} 402 | }} 403 | }}", struct_id, field_ty, field_name)?; 404 | write!(self.out, " 405 | impl {} {{ 406 | pub fn into_inner(self) -> {} {{ 407 | self.{} 408 | }} 409 | }}", struct_id, field_ty, field_name)?; 410 | } 411 | } 412 | 413 | Ok(()) 414 | } 415 | 416 | fn process_unions(&mut self) -> io::Result<()> { 417 | for u in self.unions.values() { 418 | let discrim = u.discriminator.as_ref().map(|s| &s[..]).unwrap_or("type"); 419 | let type_id = type_identifier(&u.id); 420 | write!(self.out, " 421 | #[derive(Debug, Clone, Serialize, Deserialize)] 422 | #[serde(tag = \"{}\")] 423 | pub enum {} {{ 424 | ", discrim, type_id)?; 425 | 426 | let (create_base, base, fields, enums) = match &u.base { 427 | spec::DataOrType::Data(data) if data.fields.len() > 2 => (true, Some(spec::Value { 428 | name: "base".into(), 429 | ty: spec::Type { 430 | name: format!("{}Base", type_id), 431 | is_array: false, 432 | conditional: None, 433 | features: Default::default(), 434 | }, 435 | optional: false, 436 | }), &data.fields, &self.enums), 437 | spec::DataOrType::Data(data) => (false, data.fields.iter() 438 | .find(|f| f.name != discrim).cloned(), &data.fields, &self.enums), 439 | spec::DataOrType::Type(ty) => { 440 | let base = spec::Value { 441 | name: "base".into(), 442 | ty: ty.clone(), 443 | optional: false, 444 | }; 445 | 446 | let enums = &self.enums; 447 | let ty = self.types.get_mut(&ty.name).ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("could not find qapi type {}", ty.name)))?; 448 | for field in &ty.data.fields { 449 | if field.name == discrim { 450 | self.struct_discriminators.insert(ty.id.clone(), field.name.clone()); 451 | } 452 | } 453 | (false, if ty.data.fields.len() <= 1 { None } else { Some(base) }, &ty.data.fields, enums) 454 | }, 455 | }; 456 | let base_fields = fields.iter().filter(|f| f.name != discrim); 457 | 458 | let discrim_ty = fields.iter().find(|f| f.name == discrim).map(|f| &f.ty); 459 | let discrim_ty = match discrim_ty { 460 | Some(ty) => ty, 461 | None => panic!("missing discriminator type for {}", u.id), 462 | }; 463 | let discrim_enum = match enums.get(&discrim_ty.name) { 464 | Some(e) => e, 465 | None => panic!("missing discriminator enum type {}", discrim_ty.name), 466 | }; 467 | 468 | let variants: Vec<_> = u.data.fields.iter().map(|f| 469 | (discrim_enum.data.iter().find(|v| f.name == v.as_ref()).expect("discriminator"), Some(f)) 470 | ).collect(); 471 | let variants: Vec<_> = variants.iter().copied().chain( 472 | discrim_enum.data.iter() 473 | .filter(|v| !variants.iter().any(|(vf, _)| vf.as_ref() == v.as_ref())) 474 | .map(|v| (v, None)) 475 | ).collect(); 476 | for &(variant_name, variant) in &variants { 477 | if let Some(variant) = variant { 478 | assert!(!variant.optional); 479 | assert!(!variant.ty.is_array); 480 | } 481 | 482 | write!(self.out, "\t#[serde(rename = \"{}\")]\n\t{}", variant_name, type_identifier(&variant_name))?; 483 | 484 | let field = variant.map(|variant| spec::Value { 485 | name: variant_name.to_string().clone(), 486 | ty: variant.ty.clone(), 487 | optional: false, 488 | }); 489 | match (&base, &field) { 490 | (Some(base), Some(field)) => { 491 | writeln!(self.out, " {{")?; 492 | writeln!(self.out, "\t\t{}{},", 493 | if base.name == "base" { "#[serde(flatten)] " } else { "" }, 494 | valuety(&base, false, &u.id) 495 | )?; 496 | writeln!(self.out, "\t\t#[serde(flatten)] {},", valuety(&field, false, &u.id))?; 497 | writeln!(self.out, "\t}},")?; 498 | }, 499 | (Some(field), None) | (None, Some(field)) => 500 | writeln!(self.out, "({}),", typename(&field.ty))?, 501 | (None, None) => 502 | writeln!(self.out, ",")?, 503 | } 504 | } 505 | writeln!(self.out, "}}")?; 506 | 507 | if create_base { 508 | write!(self.out, " 509 | #[derive(Debug, Clone, Serialize, Deserialize)] 510 | pub struct {} {{ 511 | ", base.as_ref().unwrap().ty.name)?; 512 | for field in base_fields.clone() { 513 | writeln!(self.out, "\t{},", valuety(&field, true, &u.id))?; 514 | } 515 | writeln!(self.out, "}}")?; 516 | } 517 | 518 | write!(self.out, " 519 | impl {} {{ 520 | pub fn {}(&self) -> {} {{ 521 | match *self {{ 522 | ", type_identifier(&u.id), identifier(&discrim), type_identifier(&discrim_ty.name))?; 523 | for &(variant_name, _) in &variants { 524 | writeln!(self.out, " 525 | {}::{} {{ .. }} => {}::{},", type_identifier(&u.id), type_identifier(&variant_name), type_identifier(&discrim_ty.name), type_identifier(&variant_name))?; 526 | } 527 | writeln!(self.out, " 528 | }} 529 | }} 530 | }}")?; 531 | 532 | let mut duptypes = HashSet::new(); 533 | let mut dups = HashSet::new(); 534 | for variant in &u.data.fields { 535 | if duptypes.contains(&&variant.ty.name) { 536 | dups.insert(&variant.ty.name); 537 | } else { 538 | duptypes.insert(&variant.ty.name); 539 | } 540 | } 541 | for variant in &u.data.fields { 542 | if dups.contains(&&variant.ty.name) { 543 | continue 544 | } 545 | let variant_ty = typename(&variant.ty); 546 | let variant_name = type_identifier(&variant.name); 547 | match &base { 548 | None => { 549 | write!(self.out, " 550 | impl From<{}> for {} {{ 551 | fn from(val: {}) -> Self {{ 552 | Self::{}(val) 553 | }} 554 | }} 555 | ", variant_ty, type_id, variant_ty, variant_name)?; 556 | let ty = self.types.get(&variant.ty.name) 557 | .map(|ty| ty.wrapper_type()) 558 | .or_else(|| self.unions.get(&variant.ty.name) 559 | .map(|_e| None) 560 | ) 561 | .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("could not find qapi type {}, needed by {}", variant.ty.name, u.id)))?; 562 | if let Some(newtype) = ty { 563 | let newtype_ty = typename(&newtype.ty); 564 | write!(self.out, " 565 | impl From<{}> for {} {{ 566 | fn from(val: {}) -> Self {{ 567 | Self::{}({}::from(val)) 568 | }} 569 | }} 570 | ", newtype_ty, type_id, newtype_ty, variant_name, variant_ty)?; 571 | } 572 | }, 573 | Some(base) => { 574 | let base_ty = typename(&base.ty); 575 | let boxed = type_id == base_ty; 576 | let base_into = match (base.optional, boxed) { 577 | (false, false) => "val.1", 578 | (false, true) | (true, false) => "val.1.into()", 579 | (true, true) => "Some(Box::new(val.1))", 580 | }; 581 | write!(self.out, " 582 | impl From<({}, {})> for {} {{ 583 | fn from(val: ({}, {})) -> Self {{ 584 | Self::{} {{ 585 | {}: val.0, 586 | {}: {}, 587 | ", variant_ty, base_ty, type_id, variant_ty, base_ty, type_identifier(&variant.name), identifier(&variant.name), identifier(&base.name), base_into)?; 588 | write!(self.out, " 589 | }} 590 | }} 591 | }} 592 | ")?; 593 | }, 594 | } 595 | } 596 | } 597 | 598 | Ok(()) 599 | } 600 | 601 | fn process_events(&mut self) -> io::Result<()> { 602 | writeln!(self.out, " 603 | #[derive(Debug, Clone, Serialize, Deserialize)] 604 | #[serde(tag = \"event\")] 605 | pub enum Event {{")?; 606 | for event in &self.events { 607 | let id = event_identifier(&event.id); 608 | writeln!(self.out, "\t#[serde(rename = \"{}\")] {} {{ 609 | {} data: {}, 610 | timestamp: ::qapi_spec::Timestamp, 611 | }},", event.id, id, if event.data.is_empty() { "#[serde(default)] " } else { "" }, id)?; 612 | } 613 | writeln!(self.out, "}}")?; 614 | 615 | writeln!(self.out, " 616 | impl Event {{ 617 | pub fn timestamp(&self) -> ::qapi_spec::Timestamp {{ 618 | match *self {{")?; 619 | for event in &self.events { 620 | writeln!(self.out, "Event::{} {{ timestamp, .. }} => timestamp,", event_identifier(&event.id))?; 621 | } 622 | writeln!(self.out, " 623 | }} 624 | }} 625 | }}")?; 626 | Ok(()) 627 | } 628 | } 629 | 630 | fn include(context: &mut Context, repo: &mut QemuFileRepo, path: &str) -> io::Result<()> { 631 | let include_path = repo.context().join(path); 632 | if context.included.contains(&include_path) { 633 | return Ok(()) 634 | } 635 | context.included.insert(include_path); 636 | 637 | let (mut repo, str) = repo.include(path)?; 638 | for item in Parser::from_string(Parser::strip_comments(&str)) { 639 | context.process(item?)?; 640 | } 641 | 642 | while !context.includes.is_empty() { 643 | let includes: Vec<_> = context.includes.drain(..).collect(); 644 | 645 | for inc in includes { 646 | include(context, &mut repo, &inc)?; 647 | } 648 | } 649 | 650 | Ok(()) 651 | } 652 | 653 | pub fn codegen, O: AsRef>(schema_path: S, out_path: O, command_trait: String) -> io::Result> { 654 | let mut repo = QemuFileRepo::new(schema_path.as_ref()); 655 | { 656 | let mut context = Context::new(File::create(out_path)?, command_trait); 657 | include(&mut context, &mut repo, "qapi-schema.json")?; 658 | context.process_unions()?; 659 | context.process_structs()?; 660 | context.process_events()?; 661 | Ok(context.included) 662 | } 663 | } 664 | --------------------------------------------------------------------------------