Lines
6.09 %
Functions
3.31 %
Branches
100 %
//! Thread callback information and utilities for azul-layout
//!
//! This module provides thread-related callback structures for background tasks
//! that need to interact with the UI thread and query layout information.
#[cfg(feature = "std")]
use alloc::sync::Arc;
use std::sync::{
mpsc::{channel, Receiver, Sender},
Mutex,
};
use std::thread::{self, JoinHandle};
use azul_core::{
callbacks::Update,
refany::{OptionRefAny, RefAny},
task::{
CheckThreadFinishedCallback, CheckThreadFinishedCallbackType, LibrarySendThreadMsgCallback,
LibrarySendThreadMsgCallbackType, OptionThreadSendMsg, ThreadId, ThreadReceiver,
ThreadReceiverDestructorCallback, ThreadReceiverInner, ThreadRecvCallback, ThreadSendMsg,
},
use crate::callbacks::CallbackInfo;
// Types that need to be defined locally (not in azul-core)
/// Message that is sent back from the running thread to the main thread
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[repr(C, u8)]
pub enum ThreadReceiveMsg {
WriteBack(ThreadWriteBackMsg),
Update(Update),
}
pub enum OptionThreadReceiveMsg {
None,
Some(ThreadReceiveMsg),
impl From<Option<ThreadReceiveMsg>> for OptionThreadReceiveMsg {
fn from(inner: Option<ThreadReceiveMsg>) -> Self {
match inner {
None => OptionThreadReceiveMsg::None,
Some(v) => OptionThreadReceiveMsg::Some(v),
impl OptionThreadReceiveMsg {
pub fn into_option(self) -> Option<ThreadReceiveMsg> {
match self {
OptionThreadReceiveMsg::None => None,
OptionThreadReceiveMsg::Some(v) => Some(v),
pub fn as_ref(&self) -> Option<&ThreadReceiveMsg> {
/// Message containing writeback data and callback
#[repr(C)]
pub struct ThreadWriteBackMsg {
pub refany: RefAny,
pub callback: WriteBackCallback,
impl ThreadWriteBackMsg {
pub fn new<C: Into<WriteBackCallback>>(callback: C, data: RefAny) -> Self {
Self {
refany: data,
callback: callback.into(),
/// ThreadSender allows sending messages from the background thread to the main thread
#[derive(Debug)]
pub struct ThreadSender {
pub ptr: alloc::boxed::Box<Arc<Mutex<ThreadSenderInner>>>,
#[cfg(not(feature = "std"))]
pub ptr: *const core::ffi::c_void,
pub run_destructor: bool,
/// For FFI: stores the foreign callable (e.g., PyFunction)
pub ctx: OptionRefAny,
impl Clone for ThreadSender {
fn clone(&self) -> Self {
ptr: self.ptr.clone(),
run_destructor: true,
ctx: self.ctx.clone(),
impl Drop for ThreadSender {
fn drop(&mut self) {
self.run_destructor = false;
impl ThreadSender {
pub fn new(_t: ThreadSenderInner) -> Self {
ptr: core::ptr::null(),
run_destructor: false,
ctx: OptionRefAny::None,
pub fn new(t: ThreadSenderInner) -> Self {
ptr: alloc::boxed::Box::new(Arc::new(Mutex::new(t))),
/// Get the FFI context (e.g., Python callable)
pub fn get_ctx(&self) -> OptionRefAny {
self.ctx.clone()
pub fn send(&mut self, _msg: ThreadReceiveMsg) -> bool {
false
pub fn send(&mut self, msg: ThreadReceiveMsg) -> bool {
let ts = match self.ptr.lock().ok() {
Some(s) => s,
None => return false,
(ts.send_fn.cb)(ts.ptr.as_ref() as *const _ as *const core::ffi::c_void, msg)
/// Inner state of a `ThreadSender`, holding the channel sender and associated callbacks
#[cfg_attr(not(feature = "std"), derive(PartialEq, PartialOrd, Eq, Ord))]
pub struct ThreadSenderInner {
pub ptr: alloc::boxed::Box<Sender<ThreadReceiveMsg>>,
pub send_fn: ThreadSendCallback,
pub destructor: ThreadSenderDestructorCallback,
unsafe impl Send for ThreadSenderInner {}
impl core::hash::Hash for ThreadSenderInner {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
(self.ptr.as_ref() as *const _ as usize).hash(state);
impl PartialEq for ThreadSenderInner {
fn eq(&self, other: &Self) -> bool {
(self.ptr.as_ref() as *const _ as usize) == (other.ptr.as_ref() as *const _ as usize)
impl Eq for ThreadSenderInner {}
impl PartialOrd for ThreadSenderInner {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(
(self.ptr.as_ref() as *const _ as usize)
.cmp(&(other.ptr.as_ref() as *const _ as usize)),
)
impl Ord for ThreadSenderInner {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
(self.ptr.as_ref() as *const _ as usize).cmp(&(other.ptr.as_ref() as *const _ as usize))
impl Drop for ThreadSenderInner {
(self.destructor.cb)(self);
/// Callback for sending messages from thread to main thread
pub type ThreadSendCallbackType = extern "C" fn(*const core::ffi::c_void, ThreadReceiveMsg) -> bool;
pub struct ThreadSendCallback {
pub cb: ThreadSendCallbackType,
impl core::fmt::Debug for ThreadSendCallback {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "ThreadSendCallback {{ cb: {:p} }}", self.cb as *const ())
impl Clone for ThreadSendCallback {
Self { cb: self.cb }
impl PartialEq for ThreadSendCallback {
self.cb as usize == other.cb as usize
impl Eq for ThreadSendCallback {}
impl PartialOrd for ThreadSendCallback {
Some(self.cmp(other))
impl Ord for ThreadSendCallback {
(self.cb as usize).cmp(&(other.cb as usize))
/// Destructor callback for ThreadSender
pub type ThreadSenderDestructorCallbackType = extern "C" fn(*mut ThreadSenderInner);
pub struct ThreadSenderDestructorCallback {
pub cb: ThreadSenderDestructorCallbackType,
impl core::fmt::Debug for ThreadSenderDestructorCallback {
write!(
f,
"ThreadSenderDestructorCallback {{ cb: {:p} }}",
self.cb as *const ()
impl Clone for ThreadSenderDestructorCallback {
impl PartialEq for ThreadSenderDestructorCallback {
impl Eq for ThreadSenderDestructorCallback {}
impl PartialOrd for ThreadSenderDestructorCallback {
impl Ord for ThreadSenderDestructorCallback {
/// Callback that runs when a thread receives a `WriteBack` message
///
/// This callback runs on the main UI thread and has access to:
/// - The thread's original data
/// - Data sent back from the background thread
/// - Full CallbackInfo for DOM queries and UI updates
pub type WriteBackCallbackType = extern "C" fn(
/* original thread data */ RefAny,
/* data to write back */ RefAny,
/* callback info */ CallbackInfo,
) -> Update;
/// Callback that can run when a thread receives a `WriteBack` message
pub struct WriteBackCallback {
pub cb: WriteBackCallbackType,
/// Native Rust code sets this to None
impl WriteBackCallback {
/// Create a new WriteBackCallback
pub fn new(cb: WriteBackCallbackType) -> Self {
cb,
/// Invoke the callback
pub fn invoke(
&self,
thread_data: RefAny,
writeback_data: RefAny,
callback_info: CallbackInfo,
) -> Update {
(self.cb)(thread_data, writeback_data, callback_info)
impl core::fmt::Debug for WriteBackCallback {
write!(f, "WriteBackCallback {{ cb: {:p} }}", self.cb as *const ())
impl Clone for WriteBackCallback {
cb: self.cb,
impl From<WriteBackCallbackType> for WriteBackCallback {
fn from(cb: WriteBackCallbackType) -> Self {
impl PartialEq for WriteBackCallback {
impl Eq for WriteBackCallback {}
impl PartialOrd for WriteBackCallback {
(self.cb as usize).partial_cmp(&(other.cb as usize))
impl Ord for WriteBackCallback {
impl core::hash::Hash for WriteBackCallback {
(self.cb as usize).hash(state);
/// Callback type for the function that runs in the background thread
pub type ThreadCallbackType = extern "C" fn(RefAny, ThreadSender, ThreadReceiver);
pub struct ThreadCallback {
pub cb: ThreadCallbackType,
impl ThreadCallback {
/// Create a new ThreadCallback
pub fn new(cb: ThreadCallbackType) -> Self {
impl core::fmt::Debug for ThreadCallback {
write!(f, "ThreadCallback {{ cb: {:p} }}", self.cb as *const ())
impl Clone for ThreadCallback {
impl From<ThreadCallbackType> for ThreadCallback {
fn from(cb: ThreadCallbackType) -> Self {
impl PartialEq for ThreadCallback {
impl Eq for ThreadCallback {}
impl PartialOrd for ThreadCallback {
impl Ord for ThreadCallback {
impl core::hash::Hash for ThreadCallback {
// Host-invoker plumbing for ThreadCallback. NOTE: this callback fires
// on a worker thread (spawned by `Thread::create`), not the main
// `App.run` thread. The per-language host-invoker thunk MUST acquire
// the host VM lock before dispatching to user code:
// * CPython: PyGILState_Ensure / _Release
// * MRI Ruby: rb_thread_call_with_gvl
// * OpenJDK: AttachCurrentThread / DetachCurrentThread
// * CLR / .NET: nothing ([UnmanagedCallersOnly] auto-trampolines)
// * OCaml: caml_acquire_runtime_system / _release
// * Lua / Perl / PHP / Pharo: cannot be called from worker thread
// (single-threaded interpreter) — fall back to writeback-only
// pattern (Rust extern "C" cb on worker, host fn on main via
// WriteBackCallback).
// See `scripts/BINDING_STRATEGY_PER_LANGUAGE.md` for the lock-acquire
// table per VM.
azul_core::impl_managed_callback! {
wrapper: ThreadCallback,
info_ty: ThreadSender,
return_ty: (),
default_ret: (),
invoker_static: THREAD_CALLBACK_INVOKER,
invoker_ty: AzThreadCallbackInvoker,
thunk_fn: az_thread_callback_thunk,
setter_fn: AzApp_setThreadCallbackInvoker,
from_handle_fn: AzThreadCallback_createFromHostHandle,
extra_args: [receiver: ThreadReceiver],
/// Callback type for receiving messages from a background thread
pub type LibraryReceiveThreadMsgCallbackType =
extern "C" fn(*const core::ffi::c_void) -> OptionThreadReceiveMsg;
pub struct LibraryReceiveThreadMsgCallback {
pub cb: LibraryReceiveThreadMsgCallbackType,
impl core::fmt::Debug for LibraryReceiveThreadMsgCallback {
"LibraryReceiveThreadMsgCallback {{ cb: {:p} }}",
impl Clone for LibraryReceiveThreadMsgCallback {
/// Callback type for the destructor that cleans up a `ThreadInner`
pub type ThreadDestructorCallbackType = extern "C" fn(*mut ThreadInner);
pub struct ThreadDestructorCallback {
pub cb: ThreadDestructorCallbackType,
impl core::fmt::Debug for ThreadDestructorCallback {
"ThreadDestructorCallback {{ cb: {:p} }}",
impl Clone for ThreadDestructorCallback {
/// Wrapper around Thread because Thread needs to be clone-able
pub struct Thread {
pub ptr: alloc::boxed::Box<Arc<Mutex<ThreadInner>>>,
impl Clone for Thread {
impl Drop for Thread {
impl Thread {
pub fn new(ti: ThreadInner) -> Self {
ptr: alloc::boxed::Box::new(Arc::new(Mutex::new(ti))),
pub fn new(_ti: ThreadInner) -> Self {
/// Creates a new thread that will execute the given callback function.
/// # Arguments
/// * `thread_initialize_data` - Data passed to the callback when the thread starts
/// * `writeback_data` - Data that will be passed back when writeback messages are received
/// * `callback` - The callback to execute in the background thread
/// # Returns
/// A new Thread handle that can be added to the event loop with `CallbackInfo::add_thread`
pub fn create<C: Into<ThreadCallback>>(
thread_initialize_data: RefAny,
callback: C,
) -> Self {
create_thread_libstd(thread_initialize_data, writeback_data, callback.into())
/// A `Thread` is a separate thread that is owned by the framework.
/// In difference to a regular thread, you don't have to `await()` the result,
/// you can just hand the Thread to the framework and it will automatically
/// update the UI when the Thread is finished.
pub struct ThreadInner {
pub thread_handle: alloc::boxed::Box<Option<JoinHandle<()>>>,
pub thread_handle: *const core::ffi::c_void,
pub sender: alloc::boxed::Box<Sender<ThreadSendMsg>>,
pub sender: *const core::ffi::c_void,
pub receiver: alloc::boxed::Box<Receiver<ThreadReceiveMsg>>,
pub receiver: *const core::ffi::c_void,
pub dropcheck: alloc::boxed::Box<alloc::sync::Weak<()>>,
pub dropcheck: *const core::ffi::c_void,
pub writeback_data: RefAny,
pub check_thread_finished_fn: CheckThreadFinishedCallback,
pub send_thread_msg_fn: LibrarySendThreadMsgCallback,
pub receive_thread_msg_fn: LibraryReceiveThreadMsgCallback,
pub thread_destructor_fn: ThreadDestructorCallback,
impl ThreadInner {
/// Returns true if the Thread has been finished, false otherwise
pub fn is_finished(&self) -> bool {
(self.check_thread_finished_fn.cb)(
self.dropcheck.as_ref() as *const _ as *const core::ffi::c_void
/// Send a message to the thread
pub fn sender_send(&mut self, msg: ThreadSendMsg) -> bool {
(self.send_thread_msg_fn.cb)(
self.sender.as_ref() as *const _ as *const core::ffi::c_void,
msg,
/// Try to receive a message from the thread (non-blocking)
pub fn receiver_try_recv(&mut self) -> OptionThreadReceiveMsg {
(self.receive_thread_msg_fn.cb)(
self.receiver.as_ref() as *const _ as *const core::ffi::c_void
true
/// Send a message to the thread (no-op in no_std)
pub fn sender_send(&mut self, _msg: ThreadSendMsg) -> bool {
/// Try to receive a message from the thread (always returns None in no_std)
None.into()
impl Drop for ThreadInner {
(self.thread_destructor_fn.cb)(self);
// Default callback implementations for std
extern "C" fn default_thread_destructor_fn(thread: *mut ThreadInner) {
let thread = unsafe { &mut *thread };
if let Some(thread_handle) = thread.thread_handle.take() {
let _ = thread.sender.send(ThreadSendMsg::TerminateThread);
let _ = thread_handle.join(); // ignore the result, don't panic
extern "C" fn default_thread_destructor_fn(_thread: *mut ThreadInner) {}
extern "C" fn library_send_thread_msg_fn(
sender: *const core::ffi::c_void,
msg: ThreadSendMsg,
) -> bool {
unsafe { &*(sender as *const Sender<ThreadSendMsg>) }
.send(msg)
.is_ok()
_sender: *const core::ffi::c_void,
_msg: ThreadSendMsg,
extern "C" fn library_receive_thread_msg_fn(
receiver: *const core::ffi::c_void,
) -> OptionThreadReceiveMsg {
unsafe { &*(receiver as *const Receiver<ThreadReceiveMsg>) }
.try_recv()
.ok()
.into()
_receiver: *const core::ffi::c_void,
extern "C" fn default_send_thread_msg_fn(
msg: ThreadReceiveMsg,
unsafe { &*(sender as *const Sender<ThreadReceiveMsg>) }
_msg: ThreadReceiveMsg,
extern "C" fn default_receive_thread_msg_fn(
) -> OptionThreadSendMsg {
unsafe { &*(receiver as *const Receiver<ThreadSendMsg>) }
extern "C" fn default_check_thread_finished(dropcheck: *const core::ffi::c_void) -> bool {
let weak = unsafe { &*(dropcheck as *const alloc::sync::Weak<()>) };
weak.upgrade().is_none()
extern "C" fn default_check_thread_finished(_dropcheck: *const core::ffi::c_void) -> bool {
extern "C" fn thread_sender_drop(_: *mut ThreadSenderInner) {}
extern "C" fn thread_receiver_drop(_: *mut ThreadReceiverInner) {}
/// Function that creates a new Thread object
pub type CreateThreadCallbackType = extern "C" fn(RefAny, RefAny, ThreadCallback) -> Thread;
pub struct CreateThreadCallback {
pub cb: CreateThreadCallbackType,
impl core::fmt::Debug for CreateThreadCallback {
"CreateThreadCallback {{ cb: {:p} }}",
impl Clone for CreateThreadCallback {
impl Copy for CreateThreadCallback {}
impl PartialEq for CreateThreadCallback {
impl Eq for CreateThreadCallback {}
impl PartialOrd for CreateThreadCallback {
impl Ord for CreateThreadCallback {
impl core::hash::Hash for CreateThreadCallback {
/// Create a new thread using the standard library
pub extern "C" fn create_thread_libstd(
callback: ThreadCallback,
) -> Thread {
let (sender_receiver, receiver_receiver) = channel::<ThreadReceiveMsg>();
let mut sender_receiver = ThreadSender::new(ThreadSenderInner {
ptr: alloc::boxed::Box::new(sender_receiver),
send_fn: ThreadSendCallback {
cb: default_send_thread_msg_fn,
destructor: ThreadSenderDestructorCallback {
cb: thread_sender_drop,
});
// Set the ctx from the callback for FFI
sender_receiver.ctx = callback.ctx.clone();
let (sender_sender, receiver_sender) = channel::<ThreadSendMsg>();
let mut receiver_sender = ThreadReceiver::new(ThreadReceiverInner {
ptr: alloc::boxed::Box::new(receiver_sender),
recv_fn: ThreadRecvCallback {
cb: default_receive_thread_msg_fn,
destructor: ThreadReceiverDestructorCallback {
cb: thread_receiver_drop,
receiver_sender.ctx = callback.ctx.clone();
let thread_check = Arc::new(());
let dropcheck = Arc::downgrade(&thread_check);
let thread_handle = Some(thread::spawn(move || {
// Keep thread_check alive for the entire duration of the thread
// by binding it to a named variable (not `_` which drops immediately)
let _thread_check_guard = thread_check;
(callback.cb)(thread_initialize_data, sender_receiver, receiver_sender);
// _thread_check_guard gets dropped here, signals that the thread has finished
}));
let thread_handle: alloc::boxed::Box<Option<JoinHandle<()>>> =
alloc::boxed::Box::new(thread_handle);
let sender: alloc::boxed::Box<Sender<ThreadSendMsg>> = alloc::boxed::Box::new(sender_sender);
let receiver: alloc::boxed::Box<Receiver<ThreadReceiveMsg>> =
alloc::boxed::Box::new(receiver_receiver);
let dropcheck: alloc::boxed::Box<alloc::sync::Weak<()>> = alloc::boxed::Box::new(dropcheck);
Thread::new(ThreadInner {
thread_handle,
sender,
receiver,
writeback_data,
dropcheck,
thread_destructor_fn: ThreadDestructorCallback {
cb: default_thread_destructor_fn,
check_thread_finished_fn: CheckThreadFinishedCallback {
cb: default_check_thread_finished,
send_thread_msg_fn: LibrarySendThreadMsgCallback {
cb: library_send_thread_msg_fn,
receive_thread_msg_fn: LibraryReceiveThreadMsgCallback {
cb: library_receive_thread_msg_fn,
})
_thread_initialize_data: RefAny,
_writeback_data: RefAny,
_callback: ThreadCallback,
Thread {
#[cfg(test)]
mod tests {
use super::*;
extern "C" fn test_writeback_callback(
_thread_data: RefAny,
_callback_info: CallbackInfo,
Update::DoNothing
#[test]
fn test_writeback_callback_creation() {
let callback = WriteBackCallback::new(test_writeback_callback);
assert_eq!(callback.cb as usize, test_writeback_callback as usize);
fn test_writeback_callback_clone() {
let cloned = callback.clone();
assert_eq!(callback, cloned);
/// Optional Thread type for API compatibility
#[derive(Debug, Clone)]
pub enum OptionThread {
Some(Thread),
impl From<Option<Thread>> for OptionThread {
fn from(o: Option<Thread>) -> Self {
match o {
None => OptionThread::None,
Some(t) => OptionThread::Some(t),
impl OptionThread {
pub fn into_option(self) -> Option<Thread> {
OptionThread::None => None,
OptionThread::Some(t) => Some(t),
// ============================================================================
// Sleep utilities
/// Sleeps the current thread for the specified number of milliseconds.
/// This is a cross-platform utility that can be called from C/C++/Python.
/// * `milliseconds` - Number of milliseconds to sleep
pub fn thread_sleep_ms(milliseconds: u64) -> azul_css::corety::EmptyStruct {
std::thread::sleep(std::time::Duration::from_millis(milliseconds));
azul_css::corety::EmptyStruct::new()
/// Sleeps the current thread for the specified number of milliseconds (no-op on no_std).
pub fn thread_sleep_ms(_milliseconds: u64) -> azul_css::corety::EmptyStruct {
// No-op on no_std - can't sleep without OS
/// Sleeps the current thread for the specified number of microseconds.
/// * `microseconds` - Number of microseconds to sleep
pub fn thread_sleep_us(microseconds: u64) -> azul_css::corety::EmptyStruct {
std::thread::sleep(std::time::Duration::from_micros(microseconds));
/// Sleeps the current thread for the specified number of microseconds (no-op on no_std).
pub fn thread_sleep_us(_microseconds: u64) -> azul_css::corety::EmptyStruct {
/// Sleeps the current thread for the specified number of nanoseconds.
/// * `nanoseconds` - Number of nanoseconds to sleep
pub fn thread_sleep_ns(nanoseconds: u64) -> azul_css::corety::EmptyStruct {
std::thread::sleep(std::time::Duration::from_nanos(nanoseconds));
/// Sleeps the current thread for the specified number of nanoseconds (no-op on no_std).
pub fn thread_sleep_ns(_nanoseconds: u64) -> azul_css::corety::EmptyStruct {