ltk_webkit/wpe/
main_loop.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Wakes the host's run loop when GLib's default main context, where WebKit's events arrive, has work, so the host does not have to poll it.

use std::ffi::{ c_int, c_uint, c_void };
use std::sync::{ Arc, Condvar, Mutex, OnceLock };
use std::thread;

use glib_sys::*;

const EFD_NONBLOCK: c_int = 0o4000;
const EFD_CLOEXEC:  c_int = 0o2000000;

extern "C"
{
	fn eventfd( initval: c_uint, flags: c_int ) -> c_int;
	fn read( fd: c_int, buf: *mut c_void, count: usize ) -> isize;
	fn write( fd: c_int, buf: *const c_void, count: usize ) -> isize;
}

/// What the context waits on until its next iteration.
struct Arm
{
	fds:     Vec<GPollFD>,
	timeout: c_int,
}

struct Watch
{
	next:      Mutex<Option<Arm>>,
	armed:     Condvar,
	/// Cuts short a wait whose arm a newer one replaced.
	interrupt: c_int,
}

static WATCH: OnceLock<Arc<Watch>> = OnceLock::new();

/// Call `wake` from another thread whenever the main context has events, so the host's run loop can call [`crate::WebView::tick`] then rather than on a timer. After a call to `wake` the watch rests until the next tick, so a host that does not tick is woken once. Call it once, from the thread that ticks.
pub fn wake_on_events( wake: impl Fn() + Send + 'static )
{
	// SAFETY: a plain syscall; the descriptor lives for the process.
	let interrupt = unsafe { eventfd( 0, EFD_NONBLOCK | EFD_CLOEXEC ) };
	if interrupt < 0 { return; }
	let watch = Arc::new( Watch { next: Mutex::new( None ), armed: Condvar::new(), interrupt } );
	if WATCH.set( Arc::clone( &watch ) ).is_err() { return; }
	// SAFETY: this thread owns the context for the rest of the process, so sources attached from other threads signal its wakeup descriptor.
	unsafe { g_main_context_acquire( g_main_context_default() ); }
	thread::Builder::new()
		.name( "ltk-webkit-watch".into() )
		.spawn( move || watch_loop( &watch, wake ) )
		.ok();
	rearm();
}

/// Hand the watcher what the context waits on now. Called after every tick, once the context has dispatched what was ready.
pub( crate ) fn rearm()
{
	let Some( watch ) = WATCH.get() else { return };
	let empty = GPollFD { fd: 0, events: 0, revents: 0 };
	let mut fds     = vec![ empty; 16 ];
	let mut timeout = -1;
	// SAFETY: this thread owns the default context, as `wake_on_events` set up; `fds` holds as many records as passed.
	unsafe
	{
		let context      = g_main_context_default();
		let mut priority = 0;
		let ready        = g_main_context_prepare( context, &mut priority ) != 0;
		loop
		{
			let needed = g_main_context_query( context, priority, &mut timeout, fds.as_mut_ptr(), fds.len() as c_int ).max( 0 ) as usize;
			if needed <= fds.len()
			{
				fds.truncate( needed );
				break;
			}
			fds.resize( needed, empty );
		}
		if ready { timeout = 0; }
	}
	if let Ok( mut next ) = watch.next.lock()
	{
		*next = Some( Arm { fds, timeout } );
		watch.armed.notify_one();
	}
	let one: u64 = 1;
	// SAFETY: an eventfd takes an 8-byte counter.
	unsafe { write( watch.interrupt, &one as *const u64 as *const c_void, 8 ); }
}

fn watch_loop( watch: &Watch, wake: impl Fn() )
{
	let mut current: Option<Arm> = None;
	loop
	{
		{
			let Ok( mut next ) = watch.next.lock() else { return };
			while next.is_none() && current.is_none()
			{
				next = match watch.armed.wait( next ) { Ok( n ) => n, Err( _ ) => return };
			}
			if let Some( arm ) = next.take() { current = Some( arm ); }
		}
		let Some( arm ) = current.as_mut() else { continue };
		let mut fds = arm.fds.clone();
		fds.push( GPollFD { fd: watch.interrupt, events: G_IO_IN as u16, revents: 0 } );
		// SAFETY: `fds` holds as many records as passed.
		let ready = unsafe { g_poll( fds.as_mut_ptr(), fds.len() as c_uint, arm.timeout ) };
		if ready < 0 { continue; }
		if fds.last().is_some_and( |f| f.revents != 0 )
		{
			let mut count: u64 = 0;
			// SAFETY: an eventfd yields an 8-byte counter.
			unsafe { read( watch.interrupt, &mut count as *mut u64 as *mut c_void, 8 ); }
			continue;
		}
		current = None;
		wake();
	}
}