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 src/core/OS_utils.c, with allocation in src/core/siril_alloc.c, process spawning in src/core/siril_spawn.c, signal handling in src/core/signals.c and application-directory resolution in 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 <sys/param.h>).

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 Memory management; here we only summarise the per-OS sources that feed it. Both get_available_memory() and get_used_RAM_memory() live in src/core/OS_utils.c.

  • Linux first tries cgroups. get_available_mem_cgroups() locates the process's controller directory via find_cgroups_path() (parsing /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 /proc/meminfo (with a MemFree: fallback for old kernels and WSL2). Used memory comes from /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 /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

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 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 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, 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 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

Added in version 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 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 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 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 src/main.c and 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 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 dbghelp.h to resolve symbol names.

  • Everywhere else (where execinfo.h is available) it uses backtrace() and backtrace_symbols_fd().

Application directories and relocation

Directory resolution is in 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 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 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 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 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

Added in version 1.5.

siril_system_is_dark_mode() and siril_watch_system_appearance_changes() in 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 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 src/core/OS_utils.c (called from src/main.c at startup). These are the OS-specific companions to the general notes in 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 <Meta> on macOS so that GDK_META_MASK (Command) matches. The <Primary><Meta> rewrite is done by siril_remap_accel() in src/gui-gtk4/utils.c, applied from set_accel_map() in 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 src/core/OS_utils.c, src/core/siril_alloc.c and 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.