""" Fiji Python Mode Startup Script =============================== This script launches Fiji in Python mode, integrating ImageJ2/Fiji with Python via PyImageJ and JPype. It provides a hybrid environment where: - Java/Fiji runs in a background thread - Python runs on the main thread with Qt event loop (on macOS) - Both GUI systems (AWT/Swing and Qt) can coexist Architecture Overview --------------------- 1. Argument Parsing - Receives JVM path, JVM arguments, main class, and application arguments - Configures JPype to use the specified JVM 2. Qt Integration (macOS) - On macOS, the CoreFoundation runloop must run on the main thread for AWT/Swing - Qt provides this runloop while also enabling Qt-based tools (napari, ndv, etc.) - Main thread: Qt event loop - Background thread: Fiji/ImageJ initialization and execution 3. ImageJ Initialization - Initializes PyImageJ in interactive mode - Launches the ImageJ UI and processes command-line arguments - Stores global reference (_ij) for access from exception handlers 4. Exception Handling - Python level: sys.excepthook and threading.Thread.run patch for better error messages - C++/Objective-C level (macOS): std::terminate handler to catch NSExceptions - Provides clear, actionable error messages instead of cryptic crashes Threading Model --------------- - WITHOUT Qt: Fiji runs on main thread, blocks until quit - WITH Qt: Main thread runs Qt event loop, Fiji runs in background thread The background thread approach allows Qt GUI operations to execute on the main thread (required on macOS) while Fiji/Java code runs separately. This prevents threading violations that would otherwise crash the application. Exception Handling Strategy --------------------------- Three layers of defensive error handling: 1. sys.excepthook: Catches uncaught Python exceptions on main thread 2. Thread.run patch: Catches uncaught Python exceptions in background threads 3. C++ terminate handler: Catches NSExceptions from Qt/Cocoa threading violations The C++ handler is particularly important on macOS, where creating Qt/Cocoa widgets on the wrong thread triggers NSInternalInconsistencyException. Instead of crashing, we provide a helpful error message directing users to use @ensure_main_thread from the superqt package. """ import logging import os import sys import imagej import scyjava # Credit: https://stackoverflow.com/a/640431/1207769 from ctypes import POINTER, c_int, cast, pythonapi from pathlib import Path __version__ = "2.0.2" _logger = logging.getLogger(__name__) _ij = None _cpp_terminate_callback = None # Keep reference to prevent garbage collection def in_interactive_inspect_mode(): """Whether '-i' option is present or PYTHONINSPECT is not empty.""" if os.environ.get("PYTHONINSPECT"): return True iflag_ptr = cast(pythonapi.Py_InteractiveFlag, POINTER(c_int)) # NOTE: in Python 2.6+ ctypes.pythonapi.Py_InspectFlag > 0 # when PYTHONINSPECT set or '-i' is present return iflag_ptr.contents.value != 0 def launch_fiji(args): # Discern app directory. app_dir = Path(__file__).parent.parent.parent # Find the divider argument. divider = "--" try: div_index = args.index(divider) except AttributeError: div_index = -1 # Validate argument syntax. if len(args) < 4 or div_index < 0 or div_index > len(args) - 2: print( "Usage: fiji.py path-to-libjvm [jvm-arg1 ... jvm-argN] " f"{divider} main-class [main-arg1 ... main-argN]" ) sys.exit(1) # Parse out the arguments. libjvm_path = args[1] jvm_args = args[2:div_index] main_class = args[div_index + 1] main_args = args[div_index + 2 :] # Isolate any classpath arguments. classpath_prefix = "-Djava.class.path=" classpath_args = [arg for arg in jvm_args if arg.startswith(classpath_prefix)] jvm_args = [arg for arg in jvm_args if arg not in classpath_args] # Combine classpaths into a single list. classpath = [ el for arg in classpath_args for el in arg[len(classpath_prefix) :].split(os.path.pathsep) ] # Pass the JVM path to JPype. p = Path(libjvm_path).absolute() jvmpath = str(p) scyjava.config.add_kwargs(jvmpath=jvmpath) scyjava.config.set_java_constraints(fetch="never") # Set JAVA_HOME to encourage use of the intended JVM, # e.g. by Maven when resolving remote artifacts. # Assumes the JVM library is nestled beneath a # `lib` (Linux/macOS) or `bin` (Windows) folder. while p.name != "lib" and p.name != "bin" and p != p.parent: p = p.parent if p != p.parent: os.environ["JAVA_HOME"] = str(p.parent) scyjava.config.add_classpath(*classpath) scyjava.start_jvm(jvm_args) # Unlock reflective access to all modules. Thanks, JPMS! try: major_version = scyjava.jvm_version()[0] if major_version >= 9: ReflectionUnlocker = scyjava.jimport("org.scijava.launcher.ReflectionUnlocker") ReflectionUnlocker.unlockAll() except Exception as e: _logger.warning(e) if main_class != "org.scijava.launcher.ClassLauncher": # Launching with an alternate main class; stop here. MainClass = scyjava.jimport(main_class) MainClass.main(main_args) return None # Do early startup actions: splash screen and java check. def tryTo(f): try: f() except Exception as e: _logger.warning(e) Splash = scyjava.jimport("org.scijava.launcher.Splash") tryTo(lambda: Splash.show()) Java = scyjava.jimport("org.scijava.launcher.Java") tryTo(lambda: Java.check()) System = scyjava.jimport("java.lang.System") appName = str(System.getProperty("scijava.app.name") or "Fiji") tryTo(lambda: Splash.update(f"Launching {appName}...")) # Initialize ImageJ, wrapping the local Fiji directory. # NB: It's OK to pass `interactive` always, because when the # --headless flag is given, Fiji still ends up in headless mode. ij = imagej.init(app_dir, mode="interactive:force") # Sweet HACK ᕦ( ͡° ͜ʖ ͡°)ᕤ try: appFrame = ij.ui().getDefaultUI().getApplicationFrame() appFrame.getComponent().setTitle("(Fiji Is Just) PyImageJ") except Exception: # Too bad, so sad, we tried. pass # Perform launch actions (handle CLI args, show UI, etc.). ij.launch(main_args) # Save a global reference to the ImageJ2 gateway, in case of crash. global _ij _ij = ij return ij def maybe_block_until_quit(ij): """ If we're not in interactive mode, we might need to block to prevent the process from shutting down. """ if ij and not in_interactive_inspect_mode(): # Block until the SciJava Context is disposed and/or Java has shut down. from time import sleep ctx = ij.context() disposed = ctx.getClass().getDeclaredField("disposed") disposed.setAccessible(True) while True: try: if disposed.get(ctx): break except Exception as e: if not scyjava.jvm_started(): # JVM has already shut down. break # Something else went wrong; log it. _logger.debug(e) sleep(0.1) def try_to(do_something): """Execute a function and log any exceptions without crashing.""" try: do_something() except Exception as e: _logger.warning(e) def log_exception(context, exc_type=None, exc_value=None, exc_traceback=None): """ Log an exception with context information. Args: context: Description of where the exception occurred (e.g., "main thread", "background thread") exc_type: Exception type (or None to use sys.exc_info()) exc_value: Exception value (or None to use sys.exc_info()) exc_traceback: Exception traceback (or None to use sys.exc_info()) """ import traceback if exc_type is None: exc_type, exc_value, exc_traceback = sys.exc_info() _logger.error(f"Unhandled exception in {context}:", exc_info=(exc_type, exc_value, exc_traceback)) print(f"*** Unhandled exception in {context}:", file=sys.stderr) traceback.print_exception(exc_type, exc_value, exc_traceback) def install_python_exception_handlers(): """ Install Python exception handlers for defensive error handling. This installs two complementary handlers: 1. sys.excepthook: Catches uncaught exceptions in the main thread. - Primarily a safety net for post-Qt-event-loop exceptions - Qt bypasses this for its own exceptions (calls qFatal instead) - Unlikely to trigger in normal operation, but provides an extra layer of defense 2. threading.Thread.run patch: Catches uncaught exceptions in background threads. - Critical for the Fiji background thread where Java↔Python calls happen - Logs exceptions from Java→Python callbacks that would otherwise be silent - Prevents threads from dying without any error message Both handlers log the exception with full traceback before propagating/exiting. """ import threading import traceback # Install sys.excepthook for main thread exceptions. original_excepthook = sys.excepthook def fiji_excepthook(exc_type, exc_value, exc_traceback): """Handle uncaught exceptions in the main thread.""" log_exception("main thread", exc_type, exc_value, exc_traceback) # Call original handler to maintain default behavior. original_excepthook(exc_type, exc_value, exc_traceback) sys.excepthook = fiji_excepthook # Patch threading.Thread.run to catch exceptions in background threads. original_thread_run = threading.Thread.run def patched_thread_run(self): """Handle uncaught exceptions in background threads.""" try: original_thread_run(self) except Exception: log_exception(f"thread {self.name}") # Re-raise to maintain normal thread termination behavior. raise threading.Thread.run = patched_thread_run def install_cpp_terminate_handler(): """ Install a C++ terminate handler to catch std::terminate() calls. This is particularly important on macOS to catch NSExceptions that occur when Qt/Cocoa operations are attempted on the wrong thread. Instead of crashing with a cryptic error, we provide a clear, actionable message. This catches exceptions at the C++/Objective-C level that Python exception handlers cannot reach (e.g., NSInternalInconsistencyException from Cocoa). """ import ctypes try: # Load all symbols in the current process. libcxx = ctypes.CDLL(None) # Define the terminate handler signature: void (*)() TERMINATE_HANDLER = ctypes.CFUNCTYPE(None) def cpp_terminate_handler(): """Handle C++ terminate() calls with a clear error message.""" # Try to determine what caused terminate() to be called. exception_info = "unknown exception" is_nsexception = False try: # Try to get exception type using __cxa_current_exception_type. # This is GCC/Clang specific (Itanium C++ ABI). cxa_current_exception_type = libcxx.__cxa_current_exception_type cxa_current_exception_type.restype = ctypes.c_void_p type_info_ptr = cxa_current_exception_type() if type_info_ptr: # Get the type name from std::type_info. # type_info.name() is at offset 8 on 64-bit systems. name_ptr = ctypes.c_void_p.from_address(type_info_ptr + 8).value if name_ptr: mangled_name = ctypes.c_char_p(name_ptr).value if mangled_name: exception_info = mangled_name.decode("utf-8") # Check if it's an NSException. if b"NSException" in mangled_name or b"NSInternalInconsistencyException" in mangled_name: is_nsexception = True except Exception: # If introspection fails, assume it's NSException based on context. # We're on macOS with Qt, in a terminate handler - very likely NSException. is_nsexception = True if is_nsexception: error_msg = """ ================================================================================ FATAL ERROR: Qt/Cocoa operation attempted on wrong thread ================================================================================ A Qt or Cocoa GUI operation was attempted from a non-main thread on macOS. This is not allowed and causes the application to crash. SOLUTION: Ensure Qt/Cocoa operations run on the main thread. If you're using napari or other Qt-based tools from Fiji scripts, use the @ensure_main_thread decorator from the 'superqt' package: from superqt import ensure_main_thread @ensure_main_thread def show_data(data): napari.imshow(data) # Now safe to call from any thread show_data(my_array) For more information, see: https://github.com/pyapp-kit/superqt ================================================================================ """ else: error_msg = f""" ================================================================================ FATAL ERROR: Uncaught exception in C++ code ================================================================================ An uncaught exception was thrown in C++ code, causing std::terminate() to be called. Exception type: {exception_info} Fiji must exit to prevent further corruption. Please report this issue if you believe it is a bug. ================================================================================ """ print(error_msg, file=sys.stderr) _logger.error(f"C++ terminate() called - exception type: {exception_info}") # Also tell the user graphically. global _ij try: ui = _ij.ui() if _ij else None if ui: ui.showDialog(error_msg, "Fiji") except BaseException: pass # Exit cleanly with a non-zero status code. # We can't continue because the JVM may be in an inconsistent state. sys.exit(186) # 186 = gBI = Graphical Broken Interface ;_; # Store callback globally to prevent garbage collection. # If we don't keep a reference, Python will delete the callback object # and the C++ runtime will call a dangling pointer → crash! global _cpp_terminate_callback _cpp_terminate_callback = TERMINATE_HANDLER(cpp_terminate_handler) # Install the terminate handler using std::set_terminate. # The symbol is mangled as _ZSt13set_terminatePFvvE on macOS/Linux. libcxx._ZSt13set_terminatePFvvE.restype = ctypes.c_void_p libcxx._ZSt13set_terminatePFvvE.argtypes = [TERMINATE_HANDLER] libcxx._ZSt13set_terminatePFvvE(_cpp_terminate_callback) _logger.debug("Installed C++ terminate handler") except AttributeError: _logger.debug("Could not find std::set_terminate symbol") except Exception as e: _logger.debug(f"Failed to install C++ terminate handler: {e}") def main(args): """Main entry point for Fiji Python mode.""" # Set up logging. _debug = ( "--debug" in args or "-Dscijava.log.level=debug" in args or os.environ.get("DEBUG") ) logging.basicConfig( level=logging.DEBUG if _debug else logging.INFO, format="[%(levelname)s:%(name)s] %(message)s", ) if _debug: # Activate PyImageJ's debug mode too. from imagej import doctor doctor.debug_to_stderr() # On macOS, the CoreFoundation runloop must run on the main thread. If not, # then Java GUI elements (AWT/Swing) won't work when invoked from JPype. # One easy way to start a CoreFoundation runloop from Python is with Qt -- # which also has the advantage of enabling compatibility with Qt-based apps # such as napari and ndv. try: from qtpy.QtCore import Qt from qtpy.QtWidgets import QApplication use_qt = True except Exception as e: _logger.debug(e) use_qt = False if use_qt: import threading # Install defensive Python exception handlers for better error reporting. # These handlers deal with exceptions on both main and non-main threads. install_python_exception_handlers() # On macOS, also install a C++ terminate handler to catch NSExceptions # that occur when Qt/Cocoa operations are attempted on the wrong thread. if sys.platform == "darwin": install_cpp_terminate_handler() # Configure Qt for macOS before any QApplication creation. try_to(lambda: QApplication.setAttribute(Qt.AA_PluginApplication, True)) try_to(lambda: QApplication.setAttribute(Qt.AA_DisableSessionManager, True)) # Create QApplication on main thread. app = QApplication(args) # Prevent Qt from quitting when last Qt window closes; we want Fiji to stay running. try_to(lambda: app.setQuitOnLastWindowClosed(False)) # Launch Fiji in a background thread. def run_fiji_in_background(): ij = launch_fiji(args) maybe_block_until_quit(ij) # Signal main thread to quit Qt when Fiji closes. app.quit() fiji_thread = threading.Thread(target=run_fiji_in_background, daemon=False) fiji_thread.start() # Run Qt event loop on main thread. app.exec() # Wait for Fiji thread termination. if fiji_thread.is_alive(): fiji_thread.join() else: # No Qt available; just launch Fiji directly and hope for the best. ij = launch_fiji(args) maybe_block_until_quit(ij) if __name__ == "__main__": main(sys.argv)