Не так давно, с версии 9.2, в QEMU появилась возможность создания моделей устройств на языке Rust. Пока в режиме эксперимента.
Я люблю этот язык и не смог пройти мимо и не попробовать реализовать модель на нем. Целью преследовал обучение и удовлетворение любопытства. Я решил поделиться проделанными шагами и результатами. Возможно, это будет для кого-то полезным.
Повествование я решил разбить на несколько частей:
Разбор интеграция Rust в QEMU (В данной части представлена только он).
Добавление поддержки класса устройств PCI, написанных на Rust. На момент написания таковая реализация от разработчиков QEMU отсутствует, и пришлось добавить собственную.
Реализация устройства PCI EDU.
За основу я взял модель устройства PCI EDU, предназначенную для обучения студентов написанию драйверов для этого класса устройств. "Отлично! Классный кандидат для переписывания на Rust", - подумал я и приступил к делу. Использовался QEMU v10.0.0 как самая свежая и содержащая наиболее солидный набор оберток над C API самого QEMU.
Прежде чем перейти непосредственно к переписыванию, я захотел разобраться в этапах сборки и линковки "растовой" части QEMU. Активируется интеграция простым добавлением параметра --enable-rust
на этапе конфигурирования. Вот мои команды для настройки и сборки:
../configure --target-list=x86_64-softmmu --enable-kvm --enable-rust
make -j`nproc`
Дальше система сборки Meson все сделает, и на выходе получится исполняемый файл qemu-system-x86_64
(да, Meson имеет поддержку нескольких языков программирования, среди которых имеется Rust, что для меня было несколько неожиданным; то есть Meson выступает в качестве замены общепринятой в экосистеме Rust системе сборки Cargo).
Хорошо! Скомпилировал, включив Rust, - иду дальше. Перешел к анализу скрипта сборки meson.build
.
Первое, что бросилось в глаза, - это конфигурирование создания биндингов к коду на C:
if have_rust
# ...
bindings_rs = rust.bindgen(
input: 'rust/wrapper.h',
dependencies: common_ss.all_dependencies(),
output: 'bindings.inc.rs',
include_directories: include_directories('.', 'include'),
bindgen_version: ['>=0.60.0'],
args: bindgen_args,
)
subdir('rust')
endif
На входе берется файл rust/wrapper.h
. Тут же подключается поддиректория rust
, которая и содержит всё, связанное с Rust в QEMU. В ней имеется: модели устройств hpet
и pl011
и обвязки вокруг C API в виде крейтов qemu_api
и qemu_api_macros
. Взглянем на скрипт сборки hpet
в rust/hw/timer/hpet/meson.build
_libhpet_rs = static_library( 'hpet', files('src/lib.rs'), rust_abi: 'rust', dependencies: [qemu_api, qemu_api_macros],
)
rust_devices_ss.add(when: 'CONFIG_X_HPET_RUST', if_true: [declare_dependency( link_whole: [_libhpet_rs], dependencies: [qemu_api_macros], variables: {'crate': 'hpet'},
)])
Скрипт описывает сборку крейта в статическую библиотеку с Rust ABI и добавляет ее как зависимость в rust_devices_ss
, добавляя в variables
пару ключ-значение: 'crate': 'hpet'
(это важный момент, на который стоит обратить внимание). Пока остается неясным: как "растовые" библиотеки объединяется с основной частью QEMU? Смотрим meson.build
в корне проекта дальше и находим:
if have_rust and target_type == 'system'
target_rust = rust_devices_ss.apply(config_target, strict: false)
crates = []
foreach dep : target_rust.dependencies()
crates += dep.get_variable('crate')
endforeach
if crates.length() > 0
rlib_rs = custom_target('rust_' + target.underscorify() + '.rs',
output: 'rust_' + target.underscorify() + '.rs',
command: [rust_root_crate, crates],
capture: true,
build_by_default: true,
build_always_stale: true)
rlib = static_library('rust_' + target.underscorify(),
rlib_rs,
dependencies: target_rust.dependencies(),
rust_abi: 'c')
arch_deps += declare_dependency(link_whole: [rlib])
endif
endif
Тут происходит несколько интересных вещей.
Используется значение по ключу
crate
из спискаvariables
, про который я чуть выше писал.Определяется
custom_target
, вызывающийrust_root_crate
и передающий емуcrates
, содержащий список имен зависимостей:hpet
,pl011
и т.д. На выходе получается файл'rust_' + target.underscorify() + '.rs'
, содержащий, как следует из названия, какой-то исходный код на Rust.Определяется сборка статической библиотеки с C ABI из полученного на этапе 2 файла с названием "'rust_' + target.underscorify()" и зависимостями из
rust_device_ss
. Дальше надо понять, что делаетrust_root_crate
. Это оказалось несложно, поиски сразу привели меня к скриптуscripts/rust/rust_root_crate.sh
:
for crate in $*; do
echo "extern crate $crate;"
done
Выходит, он генерирует файл с кодом на Rust, содержащий только extern
'ы переданных ему крейтов. Из этого и собирается статическая библиотека с C ABI, описанная выше.
❯ cat rust_x86_64_softmmu.rs
extern crate hpet;
❯ ls librust_x86_64_softmmu.a
librust_x86_64_softmmu.a
Догадки оказались верны, в директории build
, в которой выполняется сборка, имеется и этот файл, и библиотека, полученная на его основе.
На этом шаге заканчивается процесс интеграции кода на Rust в QEMU - после создания библиотеки. Дальше выполняется линковка.
Так я разобрался, как происходит "скрещивание" кода на Rust с основной кодовой базой QEMU, какие имеются этапы. Вооружившись этими знаниями, я пошел дальше - к реализации враппера над классом устройств PCI.