Files
maui-linux/docs/THREADING.md
logikonline 6b45f28b4e feat(interop): add safe handle wrappers and configuration options
Implement SafeHandle wrappers for native resources (SafeGtkWidgetHandle, SafeGObjectHandle, SafeX11DisplayHandle, SafeX11CursorHandle, SafeDlopenHandle) to prevent memory leaks. Move gesture and rendering configuration from static properties to LinuxApplicationOptions for better testability and DI compatibility. Add THREADING.md and DI-MIGRATION.md documentation. Include LayoutIntegrationTests for Measure/Arrange pipeline and SkiaViewTheoryTests with parameterized test cases using [Theory] attributes.
2026-03-06 23:40:51 -05:00

249 lines
8.6 KiB
Markdown

# Threading Model and Native Handle Ownership
This document describes the threading rules, event loop architecture, and native
resource management patterns used in the maui-linux project.
## Overview
maui-linux uses a **single-threaded UI model** inherited from GTK. All UI
operations -- rendering, layout, input handling, and widget mutation -- must
execute on a single designated thread called the *GTK thread*. Background work
is permitted, but its results must be marshaled back to the GTK thread before
touching any UI state.
## The GTK Thread
At startup, `LinuxApplication.Run` initializes the dispatcher and records the
current managed thread ID:
```csharp
// LinuxApplication.cs
private static int _gtkThreadId;
private static void StartHeartbeat()
{
_gtkThreadId = Environment.CurrentManagedThreadId;
// ...
}
```
```csharp
// LinuxDispatcher.cs
public static void Initialize()
{
_mainThreadId = Environment.CurrentManagedThreadId;
_mainDispatcher = new LinuxDispatcher();
}
```
Both `_gtkThreadId` (in `LinuxApplication`) and `_mainThreadId` (in
`LinuxDispatcher`) capture the same thread -- the one that will run the event
loop. Diagnostic logging in `LogInvalidate` and `LogRequestRedraw` warns when
UI work is attempted from any other thread.
## Thread Marshaling
There are two mechanisms to marshal work onto the GTK thread.
### 1. `GLibNative.IdleAdd` / `GLibNative.TimeoutAdd` (low-level)
These wrap `g_idle_add` and `g_timeout_add` from GLib. The callback runs on the
next iteration of the GTK main loop. Returning `true` reschedules the callback;
returning `false` removes it.
```csharp
// Marshal a one-shot action to the GTK thread
GLibNative.IdleAdd(() =>
{
// Runs on the GTK thread
RequestRedrawInternal();
return false; // do not repeat
});
// Schedule a recurring action every 250 ms
GLibNative.TimeoutAdd(250, () =>
{
// heartbeat logic
return true; // keep repeating
});
```
`GLibNative` prevents the delegate from being garbage-collected by storing it
in a static `_callbacks` list (protected by `_callbackLock`). The wrapper is
removed from the list when the callback returns `false`.
`GtkNative` has its own `IdleAdd` overload with the same prevent-GC pattern
(via `_idleCallbacks`). Prefer the `GLibNative` versions for new code -- they
have proper locking and error logging.
### 2. `LinuxDispatcher` (high-level, MAUI-compatible)
`LinuxDispatcher` implements `IDispatcher` so that MAUI's `Dispatcher.Dispatch`
works correctly on Linux.
```csharp
// From any thread:
dispatcher.Dispatch(() =>
{
// Runs on the GTK thread via GLibNative.IdleAdd
});
dispatcher.DispatchDelayed(TimeSpan.FromMilliseconds(500), () =>
{
// Runs on the GTK thread after a 500 ms delay via GLibNative.TimeoutAdd
});
```
If `Dispatch` is called from the GTK thread, the action runs **synchronously**
(no round-trip through the event loop). `DispatchDelayed` always goes through
`TimeoutAdd`, regardless of calling thread.
`LinuxDispatcherTimer` wraps `GLibNative.TimeoutAdd` to implement
`IDispatcherTimer`. Stopping a timer calls `GLibNative.SourceRemove` to cancel
the GLib source.
## Event Loop
The application supports two event-loop modes, selected by
`LinuxApplicationOptions.UseGtk`.
### X11 Mode (`RunX11`)
A manual polling loop on the main thread:
```
while (_mainWindow.IsRunning)
{
_mainWindow.ProcessEvents(); // drain X11 event queue
SkiaWebView.ProcessGtkEvents(); // pump GTK for WebView support
UpdateAnimations();
Render();
Thread.Sleep(1); // yield CPU
}
```
X11 events are read with `XNextEvent` / `XPending`. GTK is pre-initialized
(`gtk_init_check`) so that WebView (WebKitGTK) works, but GTK does not own the
main loop. GTK events are drained cooperatively via `ProcessGtkEvents`.
### GTK Mode (`RunGtk`)
GTK owns the main loop:
```csharp
StartHeartbeat(); // records _gtkThreadId, starts 250 ms heartbeat
PerformGtkLayout(w, h);
_gtkWindow.RequestRedraw();
_gtkWindow.Run(); // calls gtk_main() -- blocks until quit
GtkHostService.Instance.Shutdown();
```
All rendering is driven by GTK draw callbacks. `GLibNative.IdleAdd` and
`TimeoutAdd` integrate naturally because GTK's main loop processes GLib
sources.
## Native Handle Ownership
### Raw `IntPtr` Handles (current codebase)
Most native resources are currently held as raw `IntPtr` fields. The owning
class implements `IDisposable` and frees resources in `Dispose(bool)`.
**X11 resources** (`X11Window`):
| Resource | Acquire | Release |
|---|---|---|
| `Display*` | `XOpenDisplay` | `XCloseDisplay` |
| `Window` | `XCreateSimpleWindow` | `XDestroyWindow` |
| `Cursor` | `XCreateFontCursor` | `XFreeCursor` |
| `XImage*` | `XCreateImage` | `XDestroyImage` (also frees pixel buffer) |
Release order matters: cursors must be freed **before** the display is closed,
and the window must be destroyed before the display is closed.
**GObject resources** (`GtkHostWindow` and others):
| Resource | Acquire | Release |
|---|---|---|
| GTK widget | `gtk_window_new`, `gtk_drawing_area_new`, etc. | `gtk_widget_destroy` |
| GdkPixbuf | `gdk_pixbuf_new_from_file` | `g_object_unref` |
| GtkCssProvider | `gtk_css_provider_new` | `g_object_unref` |
GObject uses **reference counting**. `g_object_unref` decrements the ref
count; when it reaches zero the object is freed. If you receive an object with
a "floating" reference (common for newly created GTK widgets), adding it to a
container sinks the reference -- the container then owns it. Only call
`g_object_unref` on objects you have explicitly ref'd or that you own.
### Signal Connection / Disconnection
GTK signals are connected with `g_signal_connect_data` and return a `ulong`
handler ID. Signals **must** be disconnected before the widget is destroyed,
or the pointers backing the managed delegates become dangling.
```csharp
// Connect (store the handler ID)
_deleteSignalId = GtkNative.g_signal_connect_data(
_window, "delete-event",
Marshal.GetFunctionPointerForDelegate(_deleteEventHandler),
IntPtr.Zero, IntPtr.Zero, 0);
// Disconnect (in Dispose, before gtk_widget_destroy)
if (_deleteSignalId != 0)
GtkNative.g_signal_handler_disconnect(_window, _deleteSignalId);
```
Keep the delegate instance alive for as long as the signal is connected (store
it as a field). If the GC collects the delegate while the signal is still
connected, GTK will call into freed memory.
### SafeHandle Wrappers (for new code)
`Native/SafeHandles.cs` provides `SafeHandle`-derived wrappers that automate
release:
| Type | Releases via |
|---|---|
| `SafeGtkWidgetHandle` | `gtk_widget_destroy` |
| `SafeGObjectHandle` | `g_object_unref` |
| `SafeX11DisplayHandle` | `XCloseDisplay` |
| `SafeX11CursorHandle` | `XFreeCursor` (requires display ptr at construction) |
| `SafeCssProviderHandle` | `g_object_unref` |
New code should prefer these over raw `IntPtr` where practical. They guarantee
release even if `Dispose` is not called (via the CLR release mechanism),
and they prevent double-free by tracking validity.
## Common Pitfalls
1. **UI work off the GTK thread.** Any call to GTK or X11 APIs, or mutation of
`SkiaView` state, from a background thread is undefined behavior. Always
marshal with `GLibNative.IdleAdd` or `LinuxDispatcher.Dispatch`. The
diagnostic warnings from `LogInvalidate` / `LogRequestRedraw` exist to catch
this -- do not ignore them.
2. **Delegate collection during signal connection.** A `GSourceFunc` or signal
callback delegate passed to native code must be stored in a field or a static
list for its entire connected lifetime. `GLibNative._callbacks` and
`GtkNative._idleCallbacks` exist for this purpose.
3. **Reentrancy in `RequestRedraw`.** The `_isRedrawing` flag guards against
recursive invalidation. Calling `RequestRedraw` from within a render
callback is safe (it will be a no-op), but scheduling further idle callbacks
from within an idle callback can lead to unbounded queue growth if not gated.
4. **X11 resource release ordering.** Cursors and windows depend on the display
connection. Always free them before calling `XCloseDisplay`. The
`X11Window.Dispose` method shows the correct order:
cursors first, then the window, then the display.
5. **`XDestroyImage` frees the pixel buffer.** When you call `XCreateImage`
with a pointer to pixel data, `XDestroyImage` will free that memory. If you
allocated the buffer with `Marshal.AllocHGlobal`, do **not** free it yourself
after `XDestroyImage`. If `XCreateImage` fails, you **must** free it
yourself.
6. **`Thread.Sleep(1)` in X11 mode.** The X11 event loop uses a 1 ms sleep to
yield CPU. This limits frame rate and increases input latency compared to
GTK mode. Be aware of this when profiling rendering performance.