OS differences -------------- Siril runs on Linux, Windows, macOS and the BSDs, and most of the code is written against glib so that it stays portable. There is, however, a residue of genuinely per-OS behaviour: memory accounting, path handling at API boundaries, process spawning, crash handling and a handful of macOS-specific GTK4 workarounds. This chapter collects those differences and organises them **by concern** rather than by operating system, so that when you touch one of these areas you can see all of the platforms at once. Most of the per-OS code lives in :file:`src/core/OS_utils.c`, with allocation in :file:`src/core/siril_alloc.c`, process spawning in :file:`src/core/siril_spawn.c`, signal handling in :file:`src/core/signals.c` and application-directory resolution in :file:`src/core/siril_app_dirs.c`. The canonical compile-time switches are ``_WIN32``, ``OS_OSX``, ``__linux__`` and ``BSD`` (the last of these is derived from ````). Memory reporting ================ Siril budgets its work against the memory the OS will actually give it, so the "available memory" query is deliberately conservative and platform-specific. The full budgeting logic is described in :ref:`memorymanagement:memory management`; here we only summarise the per-OS sources that feed it. Both ``get_available_memory()`` and ``get_used_RAM_memory()`` live in :file:`src/core/OS_utils.c`. * **Linux** first tries cgroups. ``get_available_mem_cgroups()`` locates the process's controller directory via ``find_cgroups_path()`` (parsing :file:`/proc/self/cgroup`) and reads the memory limit from either the v1 files (``memory.limit_in_bytes``, ``memory.soft_limit_in_bytes``) or the v2 files (``memory.max``, ``memory.high``, ``memory.low``). If no cgroup limit is found it falls back to the ``MemAvailable:`` line of :file:`/proc/meminfo` (with a ``MemFree:`` fallback for old kernels and WSL2). Used memory comes from :file:`/proc/self/statm`. * **macOS** uses Mach: ``host_statistics64()`` with ``HOST_VM_INFO64`` gives the free, purgeable and external page counts, and Siril also derives a second estimate from ``sysctl(HW_MEMSIZE)`` minus the in-use pages, returning the smaller of the two. Note that **purgeable** pages are counted as available. Used memory is the task's ``phys_footprint`` (or ``resident_size`` on older SDKs) via ``task_info()``. * **Windows** has no overcommit, so the binding constraint is the commit charge, not free physical RAM. ``get_available_memory()`` calls ``GlobalMemoryStatusEx()`` and takes the **smaller** of ``ullAvailPhys`` and ``ullAvailPageFile`` — otherwise a budget derived from free RAM alone would promise memory the OS refuses to deliver once the pagefile is small or disabled. Used memory is the ``WorkingSetSize`` from ``GetProcessMemoryInfo()``. * **BSD** reads ``avail memory`` from :file:`/var/run/dmesg.boot` **once** and caches it. This is the boot-time figure, not the current runtime free memory (there is a ``FIXME`` in the source acknowledging this), so BSD budgeting is effectively static. Allocation ========== :file:`src/core/siril_alloc.c` provides ``siril_malloc()``, ``siril_calloc()`` and ``siril_free()``. On most platforms these are thin wrappers over ``malloc``, ``calloc`` and ``free``. On **Windows** they are instead backed by ``VirtualAlloc`` (with ``MEM_COMMIT | MEM_RESERVE`` and ``PAGE_READWRITE``) and released with ``VirtualFree`` and ``MEM_RELEASE``, which handles very large allocations more gracefully than the CRT heap. Because ``VirtualAlloc`` already zeroes its pages, ``siril_calloc()`` does not memset on Windows. There is no ``siril_realloc()``: a block obtained from ``siril_malloc()`` must be released with ``siril_free()`` and cannot be handed to the CRT ``free()`` on Windows. Filesystems and paths ===================== The portability rules for paths are simple and non-negotiable: * Always use the glib path/filename helpers (``g_build_filename()``, ``g_path_get_dirname()``, ``g_get_user_config_dir()`` and friends). Do not hand-assemble paths with separators. * Paths inside Siril are always UTF-8. Convert to UTF-16 only at the Windows API boundary, using ``g_utf8_to_utf16()`` on the way in and ``g_utf16_to_utf8()`` on the way out. You can see this pattern in ``find_space()``, ``siril_real_path()`` and ``get_special_folder()`` in :file:`src/core/OS_utils.c`, all of which take UTF-8 in, widen it for the ``…W`` API and narrow the result back. * Open files with ``g_fopen()`` rather than the bare ``fopen()``. On Windows ``g_fopen()`` performs the UTF-8 → UTF-16 conversion for you, so filenames with non-ASCII characters work regardless of the console code page. * **Symlink and junction resolution on Windows** is handled by ``siril_real_path()`` in :file:`src/core/OS_utils.c`. It checks for ``FILE_ATTRIBUTE_REPARSE_POINT`` and, if present, resolves the target with ``GetFinalPathNameByHandleW()``, stripping the ``\\?\`` prefix from the result. On macOS ``find_space()`` resolves symbolic links itself through ``NSFileManager`` before querying free space. * **Large files.** Catalogue reads need 64-bit file offsets. Rather than rely on a portable ``fseeko``, :file:`src/io/local_catalogues.c` defines an ``fseek64`` macro: ``_fseeki64`` on Windows and ``fseeko`` elsewhere. Use ``fseek64`` when seeking within potentially large files. Free-space and path-length queries are likewise platform-split in :file:`src/core/OS_utils.c`: ``find_space()`` has variants for macOS (``NSURL`` resource values), ``statvfs``, ``statfs`` and ``GetDiskFreeSpaceExW`` on Windows; and ``get_pathmax()`` returns ``MAX_PATH`` on Windows but queries ``pathconf(_PC_PATH_MAX)`` elsewhere. Processes ========= .. versionadded:: 1.5 Never call ``g_spawn_sync()`` or ``g_spawn_async_with_pipes()`` directly to run an external program. Use the host wrappers declared in :file:`src/core/siril_spawn.h`: * ``siril_spawn_host_sync()`` * ``siril_spawn_host_async_with_pipes()`` These exist so that Siril behaves identically whether or not it is running inside a Flatpak sandbox. When built with a Flatpak id (the ``FLATPAK_ID`` macro, set from the ``flatpak-id`` meson option), the wrappers prepend ``/bin/flatpak-spawn --host --`` to the argument vector — see ``siril_flatpak_argv()`` in :file:`src/core/siril_spawn.c` — so that the child runs on the host rather than inside the sandbox. On non-Flatpak builds the wrappers are effectively straight passthroughs to the glib functions. To keep the sandboxed and non-sandboxed paths consistent, **both wrappers assert that the caller has set** ``G_SPAWN_SEARCH_PATH`` (via ``g_return_val_if_fail``). If you do not want a ``PATH`` search, pass an absolute executable path but still set the flag; the Flatpak branch clears ``G_SPAWN_SEARCH_PATH`` internally once it has rewritten the argument vector. There is also a small compatibility macro, ``siril_spawn_check_wait_status``, which maps to ``g_spawn_check_wait_status()`` on glib ≥ 2.70 and to the deprecated ``g_spawn_check_exit_status()`` on older glib. Console and standard I/O on Windows =================================== On Windows a GUI application is not attached to a console, so ``stdout`` and ``stderr`` go nowhere by default. ``ReconnectIO()`` in :file:`src/core/OS_utils.c` fixes this: it tries ``AttachConsole(ATTACH_PARENT_PROCESS)`` to reuse the parent's console (for example when Siril was launched from a terminal), and only if that fails and a new console was requested does it call ``AllocConsole()``. It then reopens ``stdout``, ``stdin`` and ``stderr`` onto the console handles with unbuffered I/O. This is called from the Windows startup paths in :file:`src/main.c` and :file:`src/main-cli.c`. Reading from standard input is abstracted by ``siril_input_stream_from_stdin()``, which builds a ``g_win32_input_stream`` on Windows and a ``g_unix_input_stream`` elsewhere. Signals and crash handling ========================== Signal setup lives in ``signals_init()`` in :file:`src/core/signals.c`. The common set installed everywhere is ``SIGABRT``, ``SIGFPE``, ``SIGSEGV``, ``SIGTERM`` and ``SIGILL``. On non-Windows platforms Siril additionally handles ``SIGHUP``, ``SIGQUIT``, ``SIGBUS``, ``SIGINT`` and ``SIGTRAP`` — and ``SIGINT`` is treated as a clean quit request, which is convenient for developers who ``Ctrl+C`` the CLI. When a fatal signal fires, ``signal_handled()`` prints a short backtrace before exiting. The backtrace source is platform-specific: * **Windows** uses ``CaptureStackBackTrace()`` together with ``SymInitialize()`` / ``SymFromAddr()`` from :file:`dbghelp.h` to resolve symbol names. * **Everywhere else** (where :file:`execinfo.h` is available) it uses ``backtrace()`` and ``backtrace_symbols_fd()``. Application directories and relocation ====================================== Directory resolution is in :file:`src/core/siril_app_dirs.c`, driven by ``initialize_siril_directories()``. The share/data and locale directories are found differently per platform: * **Windows** derives the installation directory at runtime with ``g_win32_get_package_installation_directory_of_module()`` and looks for :file:`share/…` beneath it, so the install is fully relocatable. * **macOS** bundles set the ``SIRIL_RELOCATED_RES_DIR`` environment variable (guarded by ``ENABLE_RELOCATABLE_RESOURCES``); the share and locale directories are built relative to it. * **AppImage** (Linux, also under ``ENABLE_RELOCATABLE_RESOURCES``) uses the ``APPDIR`` environment variable and looks under :file:`usr/share/…`. * Otherwise Siril falls back to the compile-time ``PACKAGE_DATA_DIR`` and ``LOCALEDIR``. The startup directory (``search_for_startup_dir()``) prefers the user's Pictures directory, then Documents, then the home directory, all via glib's ``g_get_user_special_dir()``. Config, scripts, SPCC and user-data directories are built from glib's XDG-aware ``g_get_user_config_dir()`` and ``g_get_user_data_dir()``. **Flatpak detection** is a build-time matter: the ``FLATPAK_ID`` macro is defined when the ``flatpak-id`` meson option is set, and is what the process-spawning wrappers key off (see `Processes`_). Windows also has a bundle-path helper, ``get_siril_bundle_path()``, and an executable finder, ``find_executable_in_path()``, both in :file:`src/core/OS_utils.c`; the latter deliberately strips ``mingw64`` and ``msys64`` entries out of ``PATH`` before searching, so that Siril finds host-installed tools rather than its own MSYS2 build dependencies. Special-folder queries ====================== On Windows, ``get_special_folder()`` in :file:`src/core/OS_utils.c` wraps ``SHGetSpecialFolderLocation()`` / ``SHGetPathFromIDListW()`` to resolve a CSIDL to a UTF-8 path. On other platforms the equivalent logical directories come from glib's ``g_get_user_special_dir()`` (used in ``search_for_startup_dir()``). Dark-mode detection =================== .. versionadded:: 1.5 ``siril_system_is_dark_mode()`` and ``siril_watch_system_appearance_changes()`` in :file:`src/core/OS_utils.c` provide a uniform interface over three backends: * **macOS** queries ``[NSApp effectiveAppearance]`` against ``NSAppearanceNameDarkAqua`` and subscribes to the ``AppleInterfaceThemeChangedNotification`` distributed notification for live updates. * **Windows** reads the ``AppsUseLightTheme`` value under :file:`HKCU\\…\\Themes\\Personalize` in the registry and polls it every few seconds (there is no cheap change notification), reporting changes through the registered callback. * **Linux / Unix** reads the ``color-scheme`` key of the ``org.gnome.desktop.interface`` GSettings schema (``prefer-dark`` meaning dark) and connects to its ``changed::color-scheme`` signal. If the schema is not installed on the current desktop, both functions become no-ops. macOS GTK4 workarounds ====================== GTK4 on macOS has a couple of rough edges that Siril patches from Cocoa code in :file:`src/core/OS_utils.c` (called from :file:`src/main.c` at startup). These are the OS-specific companions to the general notes in :ref:`gtk4quirks:gtk4 quirks`. * **Keyboard shortcuts.** GTK4's ``GdkMacosView`` does not override ``performKeyEquivalent:``, so ``Cmd+key`` events never reach GTK4's shortcut controller. ``siril_macos_fix_keyboard_shortcuts()`` installs a local ``NSEvent`` monitor for key-down events, and for events carrying the Command modifier on a GDK window (the content view's class name has a ``"Gdk"`` prefix) forwards them via ``keyDown:``. Because of this, Siril registers its accelerators with ```` on macOS so that ``GDK_META_MASK`` (Command) matches. The ```` → ```` rewrite is done by ``siril_remap_accel()`` in :file:`src/gui-gtk4/utils.c`, applied from ``set_accel_map()`` in :file:`src/gui-gtk4/callbacks.c`. * **Popover autohide.** An autohide ``GtkPopover`` maps as a separate focused ``NSWindow``; once its outside-click grab is lost, clicking elsewhere no longer dismisses it. ``siril_macos_fix_popover_autohide()`` installs an ``NSEvent`` monitor for mouse-down events and, when the click lands on a GTK window other than the focused popover, runs the same dismissal path the Wayland workaround uses (via ``gui_iface.dismiss_autohide_popovers``, so the Cocoa code needs no GTK headers). Native panels such as ``NSOpenPanel`` and ``NSAlert`` are left untouched because their content views do not carry the ``"Gdk"`` class-name prefix. .. tip:: **The classic** ``windows.h`` **include-order rule.** ``windows.h`` pollutes the global namespace with macros (``min``, ``max``, ``near``, ``ERROR`` and many more) that collide with glib, GTK and C++ standard-library identifiers. When a source file needs ``windows.h``, include it **first**, before the glib and Siril headers, exactly as :file:`src/core/OS_utils.c`, :file:`src/core/siril_alloc.c` and :file:`src/algos/astrometry_solver.c` do. Getting the order wrong produces baffling compile errors deep inside unrelated headers. .. note:: Windows builds are produced under MSYS2 / mingw64. If you are adding platform-specific dependencies or need to understand how the Windows and macOS images are assembled, see the continuous-integration builds chapter, which documents the MSYS2 environment, the dependency freeze and the bundling jobs.