Skip to main content

dear_imgui_rs/context/
binding.rs

1use std::cell::{Cell, RefCell};
2use std::collections::{HashMap, HashSet};
3use std::fmt;
4use std::num::NonZeroU64;
5use std::ptr;
6use std::rc::{Rc, Weak};
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::thread::ThreadId;
9
10use parking_lot::{Mutex, ReentrantMutex};
11use thiserror::Error;
12
13use crate::sys;
14
15// All safe context switching, including backend callbacks, serializes through this lock.
16pub(crate) static CTX_MUTEX: ReentrantMutex<()> = parking_lot::const_reentrant_mutex(());
17
18static CONTEXT_THREAD_OWNER: Mutex<ContextThreadOwner> =
19    parking_lot::const_mutex(ContextThreadOwner::new());
20
21static NEXT_CONTEXT_ID: AtomicU64 = AtomicU64::new(1);
22
23thread_local! {
24    // Reusing an address replaces its entry, while guards that captured the old Weak retain the
25    // old Context generation and cannot restore the reused address by mistake.
26    static MANAGED_CONTEXTS: RefCell<HashMap<usize, ManagedContextEntry>> =
27        RefCell::new(HashMap::new());
28    // Main viewport addresses are stable for the lifetime of their native Context. Keeping a
29    // generation-bound reverse index makes Viewport::is_main independent from GImGui and avoids
30    // scanning historical Context tombstones on every platform snapshot.
31    static LIVE_MAIN_VIEWPORTS: RefCell<HashMap<usize, ContextId>> =
32        RefCell::new(HashMap::new());
33    static BOUND_CONTEXT_DEPTH: Cell<usize> = const { Cell::new(0) };
34}
35
36#[derive(Debug)]
37struct ContextThreadOwner {
38    thread: Option<ThreadId>,
39    live_contexts: usize,
40}
41
42impl ContextThreadOwner {
43    const fn new() -> Self {
44        Self {
45            thread: None,
46            live_contexts: 0,
47        }
48    }
49}
50
51/// Process-global ownership of Dear ImGui's default `GImGui` storage.
52#[derive(Debug)]
53pub(crate) struct ContextThreadLease {
54    thread: ThreadId,
55}
56
57impl ContextThreadLease {
58    pub(crate) fn acquire() -> crate::error::ImGuiResult<Self> {
59        let thread = std::thread::current().id();
60        let mut owner = CONTEXT_THREAD_OWNER.lock();
61        if owner.thread.is_some_and(|current| current != thread) {
62            return Err(crate::error::ImGuiError::ContextThreadConflict);
63        }
64        owner.live_contexts = owner.live_contexts.checked_add(1).ok_or_else(|| {
65            crate::error::ImGuiError::context_creation(
66                "process Context ownership count is exhausted",
67            )
68        })?;
69        owner.thread = Some(thread);
70        Ok(Self { thread })
71    }
72}
73
74impl Drop for ContextThreadLease {
75    fn drop(&mut self) {
76        let mut owner = CONTEXT_THREAD_OWNER.lock();
77        debug_assert_eq!(owner.thread, Some(self.thread));
78        debug_assert!(owner.live_contexts > 0);
79        owner.live_contexts -= 1;
80        if owner.live_contexts == 0 {
81            owner.thread = None;
82        }
83    }
84}
85
86pub(super) fn bound_context_scope_active() -> bool {
87    BOUND_CONTEXT_DEPTH.with(|depth| depth.get() != 0)
88}
89
90/// Returns whether `viewport` is the main viewport of one of this thread's live managed Contexts.
91///
92/// The viewport wrapper does not carry an owner field, while Context binding deliberately restores
93/// the previous native Context when a safe closure returns. Keep the ownership lookup here so
94/// viewport predicates do not silently change meaning when another managed Context is current.
95pub(crate) fn viewport_is_main_viewport(viewport: *const sys::ImGuiViewport) -> bool {
96    if viewport.is_null() {
97        return false;
98    }
99
100    if LIVE_MAIN_VIEWPORTS.with(|viewports| viewports.borrow().contains_key(&(viewport as usize))) {
101        return true;
102    }
103
104    // Raw FFI users may construct a Viewport for an unmanaged Context. Preserve that unsafe
105    // escape hatch without making managed secondary viewports call into native state.
106    let _lock = CTX_MUTEX.lock();
107    let current = unsafe { sys::igGetCurrentContext() };
108    if current.is_null() {
109        return false;
110    }
111    let current_is_managed = MANAGED_CONTEXTS.with(|contexts| {
112        matches!(
113            contexts.borrow().get(&(current as usize)),
114            Some(ManagedContextEntry::Live { state, .. })
115                if state.upgrade().is_some_and(|state| {
116                    state.lifecycle() != ContextLifecycle::NativeDestroyed
117                        && state.raw_during_teardown() == current
118                })
119        )
120    });
121    if current_is_managed {
122        return false;
123    }
124
125    let main = unsafe { sys::igGetMainViewport() };
126    !main.is_null() && std::ptr::eq(viewport, main.cast_const())
127}
128
129#[derive(Clone)]
130enum ManagedContextEntry {
131    Live {
132        id: ContextId,
133        state: Weak<ContextState>,
134    },
135    Dead {
136        id: ContextId,
137    },
138}
139
140/// Process-unique identity for a Dear ImGui context.
141#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
142pub struct ContextId(NonZeroU64);
143
144impl ContextId {
145    pub(crate) fn allocate() -> Option<Self> {
146        let value = NEXT_CONTEXT_ID
147            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
148                current.checked_add(1)
149            })
150            .ok()?;
151        NonZeroU64::new(value).map(Self)
152    }
153
154    /// Returns the stable numeric identity assigned to this Context.
155    pub fn get(self) -> NonZeroU64 {
156        self.0
157    }
158}
159
160/// Lifecycle visible to persistent safe Context capabilities.
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162#[non_exhaustive]
163pub enum ContextLifecycle {
164    /// The native Context accepts ordinary safe calls.
165    Alive,
166    /// Context teardown has started; only core-created teardown access is valid.
167    Dropping,
168    /// The native Context has been destroyed and its pointer is a tombstone.
169    NativeDestroyed,
170}
171
172pub(crate) struct ContextState {
173    id: ContextId,
174    address: usize,
175    main_viewport_address: usize,
176    raw: Cell<*mut sys::ImGuiContext>,
177    lifecycle: Cell<ContextLifecycle>,
178    dockspace_submissions: RefCell<FrameIdClaims>,
179    dock_layout_applications: RefCell<FrameIdClaims>,
180}
181
182#[derive(Default)]
183struct FrameIdClaims {
184    frame: Option<i32>,
185    ids: HashSet<sys::ImGuiID>,
186}
187
188impl FrameIdClaims {
189    fn claim(&mut self, frame: i32, id: sys::ImGuiID) -> bool {
190        if self.frame != Some(frame) {
191            self.frame = Some(frame);
192            self.ids.clear();
193        }
194        self.ids.insert(id)
195    }
196
197    fn release(&mut self, frame: i32, id: sys::ImGuiID) {
198        if self.frame == Some(frame) {
199            self.ids.remove(&id);
200        }
201    }
202}
203
204impl fmt::Debug for ContextState {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        f.debug_struct("ContextState")
207            .field("id", &self.id)
208            .field("raw", &self.raw.get())
209            .field("lifecycle", &self.lifecycle.get())
210            .finish()
211    }
212}
213
214impl ContextState {
215    pub(crate) fn new(id: ContextId, raw: *mut sys::ImGuiContext) -> Rc<Self> {
216        let main_viewport = with_bound_context(raw, || unsafe { sys::igGetMainViewport() });
217        assert!(
218            !main_viewport.is_null(),
219            "new ImGui context returned a null main viewport"
220        );
221        let main_viewport_address = main_viewport as usize;
222        let state = Rc::new(Self {
223            id,
224            address: raw as usize,
225            main_viewport_address,
226            raw: Cell::new(raw),
227            lifecycle: Cell::new(ContextLifecycle::Alive),
228            dockspace_submissions: RefCell::new(FrameIdClaims::default()),
229            dock_layout_applications: RefCell::new(FrameIdClaims::default()),
230        });
231        MANAGED_CONTEXTS.with(|contexts| {
232            contexts.borrow_mut().insert(
233                raw as usize,
234                ManagedContextEntry::Live {
235                    id,
236                    state: Rc::downgrade(&state),
237                },
238            );
239        });
240        LIVE_MAIN_VIEWPORTS.with(|viewports| {
241            let mut viewports = viewports.borrow_mut();
242            debug_assert!(
243                !viewports.contains_key(&main_viewport_address),
244                "two live ImGui contexts exposed the same main viewport address"
245            );
246            viewports.insert(main_viewport_address, id);
247        });
248        state
249    }
250
251    pub(crate) fn id(&self) -> ContextId {
252        self.id
253    }
254
255    pub(crate) fn lifecycle(&self) -> ContextLifecycle {
256        self.lifecycle.get()
257    }
258
259    pub(crate) fn raw_during_teardown(&self) -> *mut sys::ImGuiContext {
260        self.raw.get()
261    }
262
263    pub(crate) fn begin_drop(&self) {
264        debug_assert_eq!(self.lifecycle.get(), ContextLifecycle::Alive);
265        self.lifecycle.set(ContextLifecycle::Dropping);
266    }
267
268    pub(crate) fn mark_native_destroyed(&self) {
269        self.unregister_main_viewport();
270        self.raw.set(ptr::null_mut());
271        self.lifecycle.set(ContextLifecycle::NativeDestroyed);
272    }
273
274    fn unregister_main_viewport(&self) {
275        let _ = LIVE_MAIN_VIEWPORTS.try_with(|viewports| {
276            let mut viewports = viewports.borrow_mut();
277            if viewports.get(&self.main_viewport_address) == Some(&self.id) {
278                viewports.remove(&self.main_viewport_address);
279            }
280        });
281    }
282}
283
284impl Drop for ContextState {
285    fn drop(&mut self) {
286        self.unregister_main_viewport();
287        let _ = MANAGED_CONTEXTS.try_with(|contexts| {
288            let mut contexts = contexts.borrow_mut();
289            let Some(entry) = contexts.get_mut(&self.address) else {
290                return;
291            };
292            let matches_generation = match entry {
293                ManagedContextEntry::Live { id, .. } | ManagedContextEntry::Dead { id } => {
294                    *id == self.id
295                }
296            };
297            if matches_generation {
298                *entry = ManagedContextEntry::Dead { id: self.id };
299            }
300        });
301    }
302}
303
304/// Failure to enter a Context through a persistent binding capability.
305#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
306#[non_exhaustive]
307pub enum ContextBindingError {
308    /// Context teardown has started, so ordinary safe access is no longer permitted.
309    #[error("Dear ImGui context teardown is in progress")]
310    Dropping,
311    /// The originating native Context no longer exists.
312    #[error("Dear ImGui context has been destroyed")]
313    NativeDestroyed,
314}
315
316/// Persistent, non-thread-safe capability for calling against one live Context.
317#[derive(Clone)]
318#[must_use]
319pub struct ContextBinding {
320    state: Weak<ContextState>,
321    id: ContextId,
322}
323
324impl fmt::Debug for ContextBinding {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        f.debug_struct("ContextBinding")
327            .field("id", &self.id)
328            .field("lifecycle", &self.lifecycle())
329            .finish()
330    }
331}
332
333impl ContextBinding {
334    pub(crate) fn new(state: &Rc<ContextState>) -> Self {
335        Self {
336            state: Rc::downgrade(state),
337            id: state.id(),
338        }
339    }
340
341    /// Returns the identity of the originating Context.
342    pub fn id(&self) -> ContextId {
343        self.id
344    }
345
346    /// Returns the latest observable lifecycle state.
347    pub fn lifecycle(&self) -> ContextLifecycle {
348        self.state
349            .upgrade()
350            .map_or(ContextLifecycle::NativeDestroyed, |state| state.lifecycle())
351    }
352
353    /// Returns true only while ordinary safe calls may enter the Context.
354    pub fn is_alive(&self) -> bool {
355        self.lifecycle() == ContextLifecycle::Alive
356    }
357
358    pub(crate) fn claim_dockspace_submission(
359        &self,
360        frame: i32,
361        id: sys::ImGuiID,
362    ) -> Option<DockspaceFrameClaim> {
363        self.claim_dockspace_frame_id(frame, id, DockspaceClaimKind::Submission)
364    }
365
366    pub(crate) fn claim_dock_layout_application(
367        &self,
368        frame: i32,
369        id: sys::ImGuiID,
370    ) -> Option<DockspaceFrameClaim> {
371        self.claim_dockspace_frame_id(frame, id, DockspaceClaimKind::LayoutApplication)
372    }
373
374    fn claim_dockspace_frame_id(
375        &self,
376        frame: i32,
377        id: sys::ImGuiID,
378        kind: DockspaceClaimKind,
379    ) -> Option<DockspaceFrameClaim> {
380        let state = self.state.upgrade()?;
381        if state.lifecycle() != ContextLifecycle::Alive {
382            return None;
383        }
384
385        let claimed = match kind {
386            DockspaceClaimKind::Submission => {
387                state.dockspace_submissions.borrow_mut().claim(frame, id)
388            }
389            DockspaceClaimKind::LayoutApplication => {
390                state.dock_layout_applications.borrow_mut().claim(frame, id)
391            }
392        };
393        if !claimed {
394            return None;
395        }
396
397        Some(DockspaceFrameClaim {
398            state: Rc::downgrade(&state),
399            frame,
400            id,
401            kind,
402            committed: false,
403        })
404    }
405
406    /// Runs a closure while the originating Context is current.
407    pub fn try_with_bound_context<R>(
408        &self,
409        f: impl FnOnce() -> R,
410    ) -> Result<R, ContextBindingError> {
411        self.try_with_bound_context_guarded(|_| f())
412    }
413
414    pub(crate) fn try_with_bound_context_guarded<R>(
415        &self,
416        f: impl FnOnce(&mut RawBoundContextGuard) -> R,
417    ) -> Result<R, ContextBindingError> {
418        let state = self
419            .state
420            .upgrade()
421            .ok_or(ContextBindingError::NativeDestroyed)?;
422        match state.lifecycle() {
423            ContextLifecycle::Alive => {}
424            ContextLifecycle::Dropping => return Err(ContextBindingError::Dropping),
425            ContextLifecycle::NativeDestroyed => {
426                return Err(ContextBindingError::NativeDestroyed);
427            }
428        }
429
430        let _lock = CTX_MUTEX.lock();
431        match state.lifecycle() {
432            ContextLifecycle::Alive => {}
433            ContextLifecycle::Dropping => return Err(ContextBindingError::Dropping),
434            ContextLifecycle::NativeDestroyed => {
435                return Err(ContextBindingError::NativeDestroyed);
436            }
437        }
438        let raw = state.raw.get();
439        if raw.is_null() {
440            return Err(ContextBindingError::NativeDestroyed);
441        }
442
443        let mut bound = RawBoundContextGuard::bind(raw);
444        Ok(f(&mut bound))
445    }
446
447    /// Runs a closure while the originating Context is current.
448    ///
449    /// # Panics
450    ///
451    /// Panics if Context teardown has started or the native Context was destroyed. Use
452    /// [`ContextBinding::try_with_bound_context`] when teardown is an expected condition.
453    pub fn with_bound_context<R>(&self, f: impl FnOnce() -> R) -> R {
454        self.try_with_bound_context(f)
455            .unwrap_or_else(|error| panic!("ContextBinding::with_bound_context(): {error}"))
456    }
457}
458
459#[derive(Clone, Copy)]
460enum DockspaceClaimKind {
461    Submission,
462    LayoutApplication,
463}
464
465pub(crate) struct DockspaceFrameClaim {
466    state: Weak<ContextState>,
467    frame: i32,
468    id: sys::ImGuiID,
469    kind: DockspaceClaimKind,
470    committed: bool,
471}
472
473impl DockspaceFrameClaim {
474    pub(crate) fn commit(mut self) {
475        self.committed = true;
476    }
477}
478
479impl Drop for DockspaceFrameClaim {
480    fn drop(&mut self) {
481        if self.committed {
482            return;
483        }
484        let Some(state) = self.state.upgrade() else {
485            return;
486        };
487        match self.kind {
488            DockspaceClaimKind::Submission => state
489                .dockspace_submissions
490                .borrow_mut()
491                .release(self.frame, self.id),
492            DockspaceClaimKind::LayoutApplication => state
493                .dock_layout_applications
494                .borrow_mut()
495                .release(self.frame, self.id),
496        }
497    }
498}
499
500/// A weak token that reports whether ordinary access to a Context is still valid.
501#[derive(Clone, Debug)]
502#[must_use]
503pub struct ContextAliveToken(ContextBinding);
504
505impl ContextAliveToken {
506    pub(crate) fn from_binding(binding: ContextBinding) -> Self {
507        Self(binding)
508    }
509
510    /// Returns true only while the originating Context is alive and not dropping.
511    pub fn is_alive(&self) -> bool {
512        self.0.is_alive()
513    }
514}
515
516pub(crate) struct RawBoundContextGuard {
517    previous: *mut sys::ImGuiContext,
518    previous_state: Option<ManagedContextEntry>,
519    restore: bool,
520}
521
522impl RawBoundContextGuard {
523    pub(crate) fn bind(target: *mut sys::ImGuiContext) -> Self {
524        BOUND_CONTEXT_DEPTH.with(|depth| {
525            depth.set(
526                depth
527                    .get()
528                    .checked_add(1)
529                    .expect("Dear ImGui Context binding depth overflowed"),
530            );
531        });
532        unsafe {
533            let previous = sys::igGetCurrentContext();
534            let restore = previous != target;
535            let previous_state = if restore {
536                MANAGED_CONTEXTS
537                    .try_with(|contexts| contexts.borrow().get(&(previous as usize)).cloned())
538                    .ok()
539                    .flatten()
540            } else {
541                None
542            };
543            if restore {
544                sys::igSetCurrentContext(target);
545            }
546            Self {
547                previous,
548                previous_state,
549                restore,
550            }
551        }
552    }
553
554    pub(crate) fn previous_context(&self) -> *mut sys::ImGuiContext {
555        self.previous
556    }
557}
558
559impl Drop for RawBoundContextGuard {
560    fn drop(&mut self) {
561        if self.restore {
562            let previous_is_valid = match self.previous_state.as_ref() {
563                None => true,
564                Some(ManagedContextEntry::Live { id, state }) => {
565                    state.upgrade().is_some_and(|state| {
566                        state.id() == *id
567                            && state.lifecycle() != ContextLifecycle::NativeDestroyed
568                            && state.raw_during_teardown() == self.previous
569                    })
570                }
571                Some(ManagedContextEntry::Dead { .. }) => false,
572            };
573            set_current_context(if previous_is_valid {
574                self.previous
575            } else {
576                ptr::null_mut()
577            });
578        }
579        BOUND_CONTEXT_DEPTH.with(|depth| {
580            let current = depth.get();
581            debug_assert!(current > 0);
582            depth.set(current - 1);
583        });
584    }
585}
586
587pub(super) fn clear_current_context() {
588    set_current_context(ptr::null_mut());
589}
590
591pub(super) fn set_current_context(ctx: *mut sys::ImGuiContext) {
592    unsafe { sys::igSetCurrentContext(ctx) }
593}
594
595pub(super) fn no_current_context() -> bool {
596    let ctx = unsafe { sys::igGetCurrentContext() };
597    ctx.is_null()
598}
599
600pub(crate) fn with_bound_context<R>(ctx: *mut sys::ImGuiContext, f: impl FnOnce() -> R) -> R {
601    let _lock = CTX_MUTEX.lock();
602    let _bound = RawBoundContextGuard::bind(ctx);
603    f()
604}