#![warn(missing_docs)]
use cfg_if::cfg_if;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct AudioThreadPriorityError {
message: String,
inner: Option<Box<dyn Error + 'static>>,
}
impl AudioThreadPriorityError {
cfg_if! {
if #[cfg(all(target_os = "linux", feature = "dbus"))] {
fn new_with_inner(message: &str, inner: Box<dyn Error>) -> AudioThreadPriorityError {
AudioThreadPriorityError {
message: message.into(),
inner: Some(inner),
}
}
}
}
fn new(message: &str) -> AudioThreadPriorityError {
AudioThreadPriorityError {
message: message.into(),
inner: None,
}
}
}
impl fmt::Display for AudioThreadPriorityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut rv = write!(f, "AudioThreadPriorityError: {}", self.message);
if let Some(inner) = &self.inner {
rv = write!(f, " ({inner})");
}
rv
}
}
impl Error for AudioThreadPriorityError {
fn description(&self) -> &str {
&self.message
}
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.inner.as_ref().map(|e| e.as_ref())
}
}
cfg_if! {
if #[cfg(any(target_os = "macos", target_os = "ios"))] {
mod rt_mach;
extern crate mach2;
extern crate libc;
use rt_mach::promote_current_thread_to_real_time_internal;
use rt_mach::demote_current_thread_from_real_time_internal;
use rt_mach::RtPriorityHandleInternal;
} else if #[cfg(target_os = "windows")] {
mod rt_win;
use rt_win::promote_current_thread_to_real_time_internal;
use rt_win::demote_current_thread_from_real_time_internal;
use rt_win::RtPriorityHandleInternal;
} else if #[cfg(all(target_os = "linux", feature = "dbus"))] {
mod rt_linux;
extern crate dbus;
extern crate libc;
use rt_linux::promote_current_thread_to_real_time_internal;
use rt_linux::demote_current_thread_from_real_time_internal;
use rt_linux::set_real_time_hard_limit_internal as set_real_time_hard_limit;
use rt_linux::get_current_thread_info_internal;
use rt_linux::promote_thread_to_real_time_internal;
use rt_linux::demote_thread_from_real_time_internal;
use rt_linux::RtPriorityThreadInfoInternal;
use rt_linux::RtPriorityHandleInternal;
#[no_mangle]
pub static ATP_THREAD_INFO_SIZE: usize = std::mem::size_of::<RtPriorityThreadInfo>();
} else if #[cfg(target_os = "linux")] {
mod rt_linux_native;
extern crate libc;
use rt_linux_native::promote_current_thread_to_real_time_internal;
use rt_linux_native::demote_current_thread_from_real_time_internal;
use rt_linux_native::set_real_time_hard_limit_internal as set_real_time_hard_limit;
use rt_linux_native::get_current_thread_info_internal;
use rt_linux_native::promote_thread_to_real_time_internal;
use rt_linux_native::demote_thread_from_real_time_internal;
use rt_linux_native::RtPriorityThreadInfoInternal;
use rt_linux_native::RtPriorityHandleInternal;
pub use rt_linux_native::set_rt_priority;
#[no_mangle]
pub static ATP_THREAD_INFO_SIZE: usize = std::mem::size_of::<RtPriorityThreadInfo>();
} else if #[cfg(target_os = "android")] {
mod rt_android;
use rt_android::promote_current_thread_to_real_time_internal;
use rt_android::demote_current_thread_from_real_time_internal;
use rt_android::RtPriorityHandleInternal;
} else {
pub struct RtPriorityHandleInternal {}
#[derive(Clone, Copy, PartialEq)]
pub struct RtPriorityThreadInfoInternal {
_dummy: u8
}
pub type RtPriorityThreadInfo = RtPriorityThreadInfoInternal;
impl RtPriorityThreadInfo {
pub fn serialize(&self) -> [u8; 1] {
[0]
}
pub fn deserialize(_: [u8; 1]) -> Self {
RtPriorityThreadInfo{_dummy: 0}
}
pub fn pid(&self) -> i32 {
-1
}
}
pub fn promote_current_thread_to_real_time_internal(_: u32, audio_samplerate_hz: u32) -> Result<RtPriorityHandle, AudioThreadPriorityError> {
if audio_samplerate_hz == 0 {
return Err(AudioThreadPriorityError{message: "sample rate is zero".to_string(), inner: None});
}
Ok(RtPriorityHandle{})
}
pub fn demote_current_thread_from_real_time_internal(_: RtPriorityHandle) -> Result<(), AudioThreadPriorityError> {
Ok(())
}
pub fn set_real_time_hard_limit(
_: u32,
_: u32,
) -> Result<(), AudioThreadPriorityError> {
Ok(())
}
pub fn get_current_thread_info_internal() -> Result<RtPriorityThreadInfo, AudioThreadPriorityError> {
Ok(RtPriorityThreadInfo{_dummy: 0})
}
pub fn promote_thread_to_real_time_internal(
_: RtPriorityThreadInfo,
_: u32,
audio_samplerate_hz: u32,
) -> Result<RtPriorityHandle, AudioThreadPriorityError> {
if audio_samplerate_hz == 0 {
return Err(AudioThreadPriorityError::new("sample rate is zero"));
}
Ok(RtPriorityHandle{})
}
pub fn demote_thread_from_real_time_internal(_: RtPriorityThreadInfo) -> Result<(), AudioThreadPriorityError> {
Ok(())
}
#[no_mangle]
pub static ATP_THREAD_INFO_SIZE: usize = std::mem::size_of::<RtPriorityThreadInfo>();
}
}
pub type RtPriorityHandle = RtPriorityHandleInternal;
cfg_if! {
if #[cfg(target_os = "linux")] {
pub type RtPriorityThreadInfo = RtPriorityThreadInfoInternal;
pub fn get_current_thread_info() -> Result<RtPriorityThreadInfo, AudioThreadPriorityError> {
get_current_thread_info_internal()
}
pub fn thread_info_serialize(
thread_info: RtPriorityThreadInfo,
) -> [u8; std::mem::size_of::<RtPriorityThreadInfo>()] {
thread_info.serialize()
}
pub fn thread_info_deserialize(
bytes: [u8; std::mem::size_of::<RtPriorityThreadInfo>()],
) -> RtPriorityThreadInfo {
RtPriorityThreadInfoInternal::deserialize(bytes)
}
#[no_mangle]
pub extern "C" fn atp_get_current_thread_info() -> *mut atp_thread_info {
match get_current_thread_info() {
Ok(thread_info) => Box::into_raw(Box::new(atp_thread_info(thread_info))),
_ => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn atp_free_thread_info(thread_info: *mut atp_thread_info) -> i32 {
if thread_info.is_null() {
return 1;
}
drop(Box::from_raw(thread_info));
0
}
#[no_mangle]
pub unsafe extern "C" fn atp_serialize_thread_info(
thread_info: *mut atp_thread_info,
bytes: *mut libc::c_void,
) {
let thread_info = &mut *thread_info;
let source = thread_info.0.serialize();
std::ptr::copy(source.as_ptr(), bytes as *mut u8, source.len());
}
#[no_mangle]
pub unsafe extern "C" fn atp_deserialize_thread_info(
in_bytes: *mut u8,
) -> *mut atp_thread_info {
let bytes = *(in_bytes as *mut [u8; std::mem::size_of::<RtPriorityThreadInfoInternal>()]);
let thread_info = RtPriorityThreadInfoInternal::deserialize(bytes);
Box::into_raw(Box::new(atp_thread_info(thread_info)))
}
pub fn promote_thread_to_real_time(
thread_info: RtPriorityThreadInfo,
audio_buffer_frames: u32,
audio_samplerate_hz: u32,
) -> Result<RtPriorityHandle, AudioThreadPriorityError> {
if audio_samplerate_hz == 0 {
return Err(AudioThreadPriorityError::new("sample rate is zero"));
}
promote_thread_to_real_time_internal(
thread_info,
audio_buffer_frames,
audio_samplerate_hz,
)
}
pub fn demote_thread_from_real_time(thread_info: RtPriorityThreadInfo) -> Result<(), AudioThreadPriorityError> {
demote_thread_from_real_time_internal(thread_info)
}
#[allow(non_camel_case_types)]
pub struct atp_thread_info(RtPriorityThreadInfo);
#[no_mangle]
pub unsafe extern "C" fn atp_promote_thread_to_real_time(
thread_info: *mut atp_thread_info,
audio_buffer_frames: u32,
audio_samplerate_hz: u32,
) -> *mut atp_handle {
let thread_info = &mut *thread_info;
match promote_thread_to_real_time(thread_info.0, audio_buffer_frames, audio_samplerate_hz) {
Ok(handle) => Box::into_raw(Box::new(atp_handle(handle))),
_ => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn atp_demote_thread_from_real_time(thread_info: *mut atp_thread_info) -> i32 {
if thread_info.is_null() {
return 1;
}
let thread_info = (*thread_info).0;
match demote_thread_from_real_time(thread_info) {
Ok(_) => 0,
_ => 1,
}
}
#[no_mangle]
pub extern "C" fn atp_set_real_time_limit(audio_buffer_frames: u32,
audio_samplerate_hz: u32) -> i32 {
let r = set_real_time_hard_limit(audio_buffer_frames, audio_samplerate_hz);
if r.is_err() {
return 1;
}
0
}
}
}
pub fn promote_current_thread_to_real_time(
audio_buffer_frames: u32,
audio_samplerate_hz: u32,
) -> Result<RtPriorityHandle, AudioThreadPriorityError> {
if audio_samplerate_hz == 0 {
return Err(AudioThreadPriorityError::new("sample rate is zero"));
}
promote_current_thread_to_real_time_internal(audio_buffer_frames, audio_samplerate_hz)
}
pub fn demote_current_thread_from_real_time(
handle: RtPriorityHandle,
) -> Result<(), AudioThreadPriorityError> {
demote_current_thread_from_real_time_internal(handle)
}
#[allow(non_camel_case_types)]
pub struct atp_handle(RtPriorityHandle);
#[no_mangle]
pub extern "C" fn atp_promote_current_thread_to_real_time(
audio_buffer_frames: u32,
audio_samplerate_hz: u32,
) -> *mut atp_handle {
match promote_current_thread_to_real_time(audio_buffer_frames, audio_samplerate_hz) {
Ok(handle) => Box::into_raw(Box::new(atp_handle(handle))),
_ => std::ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn atp_demote_current_thread_from_real_time(handle: *mut atp_handle) -> i32 {
assert!(!handle.is_null());
let handle = Box::from_raw(handle);
match demote_current_thread_from_real_time(handle.0) {
Ok(_) => 0,
_ => 1,
}
}
#[no_mangle]
pub unsafe extern "C" fn atp_free_handle(handle: *mut atp_handle) -> i32 {
if handle.is_null() {
return 1;
}
let _handle = Box::from_raw(handle);
0
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "terminal-logging")]
use simple_logger;
#[cfg(target_os = "linux")]
fn rt_scheduling_available() -> bool {
match promote_current_thread_to_real_time(0, 44100) {
Ok(handle) => {
demote_current_thread_from_real_time(handle)
.expect("demotion after a successful promotion should succeed");
true
}
Err(e) => {
eprintln!("real-time scheduling unavailable: {e}");
false
}
}
}
#[cfg(target_os = "linux")]
static RT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn it_works() {
#[cfg(target_os = "linux")]
let _rt_lock = RT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
#[cfg(feature = "terminal-logging")]
simple_logger::init().unwrap();
{
assert!(promote_current_thread_to_real_time(0, 0).is_err());
}
#[cfg(target_os = "linux")]
if !rt_scheduling_available() {
eprintln!("skipping it_works: real-time scheduling is not permitted here");
return;
}
{
match promote_current_thread_to_real_time(0, 44100) {
Ok(rt_prio_handle) => {
let rv = demote_current_thread_from_real_time(rt_prio_handle);
assert!(rv.is_ok());
}
Err(e) => {
panic!("{}", e);
}
}
}
{
match promote_current_thread_to_real_time(512, 44100) {
Ok(rt_prio_handle) => {
let rv = demote_current_thread_from_real_time(rt_prio_handle);
assert!(rv.is_ok());
}
Err(e) => {
panic!("{}", e);
}
}
}
{
match promote_current_thread_to_real_time(0, 192000) {
Ok(rt_prio_handle) => {
let rv = demote_current_thread_from_real_time(rt_prio_handle);
assert!(rv.is_ok());
}
Err(e) => {
panic!("{}", e);
}
}
}
{
match promote_current_thread_to_real_time(8192, 48000) {
Ok(rt_prio_handle) => {
let rv = demote_current_thread_from_real_time(rt_prio_handle);
assert!(rv.is_ok());
}
Err(e) => {
panic!("{}", e);
}
}
}
{
match promote_current_thread_to_real_time(512, 44100) {
Ok(_) => {}
Err(e) => {
panic!("{}", e);
}
}
}
}
fn promote_and_demote_once() {
match promote_current_thread_to_real_time(0, 44100) {
Ok(handle) => {
demote_current_thread_from_real_time(handle)
.expect("demotion after a successful promotion should succeed");
}
Err(e) => {
panic!("{}", e);
}
}
}
#[test]
fn it_works_in_different_threads() {
#[cfg(target_os = "linux")]
let _rt_lock = RT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
#[cfg(target_os = "linux")]
if !rt_scheduling_available() {
eprintln!(
"skipping it_works_in_different_threads: real-time scheduling is not permitted here"
);
return;
}
const THREADS: usize = if cfg!(target_os = "linux") { 4 } else { 32 };
let handles: Vec<_> = (0..THREADS)
.map(|_| std::thread::spawn(promote_and_demote_once))
.collect();
for handle in handles {
handle.join().unwrap()
}
}
cfg_if! {
if #[cfg(target_os = "linux")] {
use nix::unistd::*;
use nix::sys::signal::*;
#[cfg(not(feature = "dbus"))]
use nix::sys::wait::*;
#[test]
fn test_linux_api() {
let _rt_lock = RT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
{
let info = get_current_thread_info().unwrap();
let bytes = info.serialize();
let info2 = RtPriorityThreadInfo::deserialize(bytes);
assert!(info == info2);
}
{
let info = get_current_thread_info().unwrap();
let bytes = thread_info_serialize(info);
let info2 = thread_info_deserialize(bytes);
assert!(info == info2);
}
if !rt_scheduling_available() {
eprintln!("skipping test_linux_api: real-time scheduling is not permitted here");
return;
}
{
let info = get_current_thread_info().unwrap();
match promote_thread_to_real_time(info, 512, 44100) {
Ok(_) => { }
Err(e) => {
panic!("{}", e);
}
}
}
}
#[test]
fn test_remote_promotion() {
let _rt_lock = RT_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
if !rt_scheduling_available() {
eprintln!("skipping test_remote_promotion: real-time scheduling is not permitted here");
return;
}
let (rd, wr) = pipe().unwrap();
match unsafe { fork().expect("fork failed") } {
ForkResult::Parent{ child } => {
eprintln!("Parent PID: {}", getpid());
let mut bytes = [0_u8; std::mem::size_of::<RtPriorityThreadInfo>()];
match read(rd, &mut bytes) {
Ok(_) => {
let info = RtPriorityThreadInfo::deserialize(bytes);
match promote_thread_to_real_time(info, 0, 44100) {
Ok(_) => {
eprintln!("thread promotion in the child from the parent succeeded");
}
Err(e) => {
kill(child, SIGKILL).expect("Could not kill the child?");
if cfg!(feature = "dbus") {
panic!("{}", e);
} else {
eprintln!("skipping test_remote_promotion: promoting a thread in another process needs elevated privilege ({e})");
return;
}
}
}
}
Err(e) => {
eprintln!("could not read from the pipe: {}", e);
}
}
kill(child, SIGKILL).expect("Could not kill the child?");
}
ForkResult::Child => {
let r = set_real_time_hard_limit(0, 44100);
if r.is_err() {
eprintln!("Could not set RT limit, the test will fail.");
}
eprintln!("Child pid: {}", getpid());
let info = get_current_thread_info().unwrap();
let bytes = info.serialize();
match write(wr, &bytes) {
Ok(_) => {
loop {
std::thread::sleep(std::time::Duration::from_millis(1000));
eprintln!("child sleeping, waiting to be promoted...");
}
}
Err(_) => {
eprintln!("write error on the pipe.");
}
}
}
}
}
cfg_if! {
if #[cfg(not(feature = "dbus"))] {
const SCHED_RESET_ON_FORK: libc::c_int = 0x4000_0000;
const PASSED: i32 = 0;
const FAILED: i32 = 1;
const SKIPPED: i32 = 2;
fn rtprio_limit() -> libc::rlimit {
let mut lim = unsafe { std::mem::zeroed::<libc::rlimit>() };
assert_eq!(unsafe { libc::getrlimit(libc::RLIMIT_RTPRIO, &mut lim) }, 0);
lim
}
fn set_rtprio_soft(soft: libc::rlim_t) {
let mut lim = rtprio_limit();
lim.rlim_cur = soft;
assert_eq!(unsafe { libc::setrlimit(libc::RLIMIT_RTPRIO, &lim) }, 0);
}
fn current_scheduler() -> (libc::c_int, libc::c_int) {
let mut policy = 0;
let mut param = unsafe { std::mem::zeroed::<libc::sched_param>() };
assert_eq!(
unsafe {
libc::pthread_getschedparam(
libc::pthread_self(),
&mut policy,
&mut param,
)
},
0
);
(policy, param.sched_priority)
}
fn run_in_child(name: &str, checks: impl FnOnce() -> i32) {
match unsafe { fork().expect("fork failed") } {
ForkResult::Parent { child } => {
match waitpid(child, None).expect("waitpid") {
WaitStatus::Exited(_, PASSED) => {}
WaitStatus::Exited(_, SKIPPED) => {
eprintln!("skipping {}: needs an unprivileged process with a real-time budget", name);
}
other => panic!("{} child reported a failure: {:?}", name, other),
}
}
ForkResult::Child => std::process::exit(checks()),
}
}
#[test]
fn test_native_promotion_honours_rlimit() {
const RT_PRIO: libc::c_int = 10;
run_in_child("test_native_promotion_honours_rlimit", || {
if unsafe { libc::geteuid() } == 0 {
return SKIPPED;
}
if rtprio_limit().rlim_max < RT_PRIO as libc::rlim_t {
return SKIPPED;
}
set_rtprio_soft(RT_PRIO as libc::rlim_t - 1);
if promote_current_thread_to_real_time(0, 44100).is_ok() {
eprintln!("promotion succeeded below the RLIMIT_RTPRIO ceiling");
return FAILED;
}
set_rtprio_soft(RT_PRIO as libc::rlim_t);
let handle = match promote_current_thread_to_real_time(0, 44100) {
Ok(handle) => handle,
Err(e) => {
eprintln!("promotion denied at the RLIMIT_RTPRIO ceiling: {e}");
return FAILED;
}
};
let (policy, prio) = current_scheduler();
if policy & !SCHED_RESET_ON_FORK != libc::SCHED_FIFO || prio != RT_PRIO {
eprintln!("unexpected scheduler after promotion: policy={policy} prio={prio}");
return FAILED;
}
if demote_current_thread_from_real_time(handle).is_err() {
eprintln!("demotion failed");
return FAILED;
}
PASSED
});
}
#[test]
fn test_native_priority_override() {
const OVERRIDE: libc::c_int = 7;
run_in_child("test_native_priority_override", || {
if unsafe { libc::geteuid() } == 0 {
return SKIPPED;
}
let hard = rtprio_limit().rlim_max;
if hard < OVERRIDE as libc::rlim_t {
return SKIPPED;
}
set_rtprio_soft(hard);
set_rt_priority(Some(OVERRIDE as u8));
let handle = match promote_current_thread_to_real_time(0, 44100) {
Ok(handle) => handle,
Err(e) => {
eprintln!("promotion denied at priority {OVERRIDE}: {e}");
return FAILED;
}
};
let (_, prio) = current_scheduler();
let result = if prio == OVERRIDE {
PASSED
} else {
eprintln!("expected priority {OVERRIDE}, got {prio}");
FAILED
};
let _ = demote_current_thread_from_real_time(handle);
result
});
}
}
}
}
}
}