diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 00000000..84f026fe --- /dev/null +++ b/src/context.rs @@ -0,0 +1,84 @@ +// nih-plug: plugins, but rewritten in Rust +// Copyright (C) 2022 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Different contexts the plugin can use to make callbacks to the host in different...contexts. + +use std::sync::Arc; + +#[cfg(all(target_family = "unix", not(target_os = "macos")))] +mod linux; + +#[cfg(all(target_family = "unix", not(target_os = "macos")))] +pub(crate) use linux::LinuxEventLoop as OsEventLoop; + +// TODO: ProcessContext for parameter automation and sending events +// TODO: GuiContext for GUI parameter automation and resizing + +pub(crate) const TASK_QUEUE_CAPACITY: usize = 512; + +/// General callbacks the plugin can make during its lifetime. This is passed to the plugin during +/// [Plugin::initialize]. +/// +/// # Safety +/// +/// This context is passed to be stored by the plugin, and it may thus not outlive the wrapper. +/// Hence the use of reference counted smart pointers. The implementing wrapper needs to be able to +/// handle concurrent requests, and it should perform the actual callback within +/// [MainThreadQueue::do_maybe_async]. +pub unsafe trait ProcessContext { + /// Update the current latency of the plugin. If the plugin is currently processing audio, then + /// this may cause audio playback to be restarted. + fn set_latency_samples(self: &Arc, samples: u32); +} + +/// A trait describing the functionality of the platform-specific event loop that can execute tasks +/// of type `T` in executor `E`. Posting a task to the queue should be realtime safe. This thread +/// queue should be created during the wrapper's initial initialization on the main thread. +/// +/// This is never used generically, but having this as a trait will cause any missing functions on +/// an implementation to show up as compiler errors even when using a different platform. +/// +/// TODO: At some point rethink the design to make it possible to have a singleton message queue for +/// all instances of a plugin. +pub(crate) trait EventLoop +where + T: Send, + E: MainThreadExecutor, +{ + /// Create a main thread tasks queue for the given executor. The thread this is called on will + /// be designated as the main thread, so this should be called when constructing the wrapper. + /// + /// TODO: Spawn, and update docs + fn new_and_spawn(executor: Arc) -> Self; + + /// Either post the function to a queue so it can be run later from the main thread using a + /// timer, or run the function directly if this is the main thread. This needs to be callable at + /// any time withotu blocking. + /// + /// If the task queue was full, then this will return false. + #[must_use] + fn do_maybe_async(&self, task: T) -> bool; + + /// Whether the calling thread is the even loop's main thread. This is usually the thread the + /// event loop instance wel initialized on. + fn is_main_thread(&self) -> bool; +} + +/// Something that can execute tasks of type `T`. +pub(crate) trait MainThreadExecutor: Send + Sync { + /// Execute a task on the current thread. + fn execute(&self, task: T); +} diff --git a/src/context/linux.rs b/src/context/linux.rs new file mode 100644 index 00000000..61bdda8f --- /dev/null +++ b/src/context/linux.rs @@ -0,0 +1,127 @@ +// nih-plug: plugins, but rewritten in Rust +// Copyright (C) 2022 Robbert van der Helm +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! An event loop impelemntation for Linux. APIs on Linux are generally thread safe, so the context +//! of a main thread does not exist there. Because of that, this mostly just serves as a way to +//! delegate expensive processing to another thread. + +use crossbeam::channel; +use std::sync::Arc; +use std::thread::{self, JoinHandle, ThreadId}; + +use crate::nih_log; + +use super::{EventLoop, MainThreadExecutor}; + +/// See [super::EventLoop]. +pub(crate) struct LinuxEventLoop { + /// The thing that ends up executing these tasks. The tasks are usually executed from the worker + /// thread, but if the current thread is the main thread then the task cna also be executed + /// directly. + executor: Arc, + + /// The ID of the main thread. In practice this is the ID of the thread that created this task + /// queue. + main_thread_id: ThreadId, + + /// A thread that act as our worker thread. When [do_maybe_async] is called, this thread will be + /// woken up to execute the task on the executor. This is wrapped in an `Option` so the thread + /// can be taken out of it and joined when this struct gets dropped. + worker_thread: Option>, + /// A channel for waking up the worker thread and having it perform one of the tasks from + /// [Message]. + worker_thread_channel: channel::Sender>, +} + +/// A message for communicating with the worker thread. +enum Message { + /// A new task for the event loop to execute. + Task(T), + /// Shut down the worker thread. + Shutdown, +} + +impl EventLoop for LinuxEventLoop +where + T: Send, + E: MainThreadExecutor, +{ + fn new_and_spawn(executor: Arc) -> Self { + let (sender, receiver) = channel::bounded(super::TASK_QUEUE_CAPACITY); + + Self { + executor: executor.clone(), + main_thread_id: thread::current().id(), + // With our drop implementation we guarentee that this thread never outlives this struct + worker_thread: Some(unsafe { + thread::Builder::new() + .name(String::from("worker")) + // FIXME: Find another way to bind a thread lifetime to this struct without a + // nightly-only fature + .spawn_unchecked(move || worker_thread(receiver, executor)) + .expect("Could not spawn worker thread") + }), + worker_thread_channel: sender, + } + } + + fn do_maybe_async(&self, task: T) -> bool { + if self.is_main_thread() { + self.executor.execute(task); + true + } else { + self.worker_thread_channel + .try_send(Message::Task(task)) + .is_ok() + } + } + + fn is_main_thread(&self) -> bool { + thread::current().id() == self.main_thread_id + } +} + +impl Drop for LinuxEventLoop { + fn drop(&mut self) { + self.worker_thread_channel + .send(Message::Shutdown) + .expect("Failed while sending worker thread shutdown request"); + if let Some(join_handle) = self.worker_thread.take() { + join_handle.join().expect("Worker thread panicked"); + } + } +} + +/// The worker thread used in [EventLoop] that executes incmoing tasks on the event loop's executor. +fn worker_thread(receiver: channel::Receiver>, executor: Arc) +where + T: Send, + E: MainThreadExecutor, +{ + loop { + match receiver.recv() { + Ok(Message::Task(task)) => executor.execute(task), + Ok(Message::Shutdown) => return, + Err(err) => { + nih_log!( + "Worker thread got disconnected unexpectedly, shutting down: {}", + err + ); + return; + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 698f4ce6..d163b628 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,10 @@ // TODO: Once everything is more fleshed out, document the basic usage of this library +// FIXME: Find an alternative for this +#![feature(thread_spawn_unchecked)] + +pub mod context; #[macro_use] pub mod debug; pub mod formatters;