Skip to content

DartBridge.hardExit, and re-pin python-build to 20260908 - #248

Merged
FeodorFitsner merged 2 commits into
mainfrom
hard-exit
Sep 9, 2026
Merged

DartBridge.hardExit, and re-pin python-build to 20260908#248
FeodorFitsner merged 2 commits into
mainfrom
hard-exit

Conversation

@FeodorFitsner

@FeodorFitsner FeodorFitsner commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Problem

dart:io's exit() runs the normal C teardown, which destroys the C++ statics inside every loaded CPython extension module. The interpreter lives on a detached thread (sp_worker) that may still be running, so that teardown can fault it: a SIGSEGV on exit from an app that had already finished its work.

The reported case died in matplotlib's ft2font looking up a pybind11 type-caster map that had just been destructed:

far: 0x0000000000000000   esr: 0x92000006 (Data Abort) byte read Translation fault

Nothing is matplotlib-specific. A second capture of the same crash showed the main thread inside SkTypefaceCache::~SkTypefaceCache() (Flutter's own Skia static) on the same __cxa_finalize_ranges pass.

Change

DartBridge.hardExit(exitCode) / DartBridge.canHardExit in dart_bridge_ffi.dart, binding serious_python_hard_exit from dart_bridge 1.9.0 (flet-dev/dart-bridge#20). It terminates without running atexit handlers or C++ static destructors, and does not return.

The symbol is resolved through the existing _lookupOrNull soft lookup, the same mechanism already used for isPythonInitialized and signalDartSession, so an app built against an older libdart_bridge still loads. canHardExit is then false, hardExit is a no-op, and the caller falls back to exit() with the pre-existing behaviour.

Version tables regenerated with dart run serious_python:gen_version_tables --release-date 20260908, moving dart_bridge_version to 1.9.0 and python_build_release_date to 20260908 across the four python_versions.properties files and python_versions.dart. No CPython or Pyodide versions change; the python-build release (flet-dev/python-build#41) exists to publish the updated manifest.

Versions and changelogs. All six packages go to 4.7.0, each with its own CHANGELOG.md entry: the substance sits in serious_python_platform_interface (where hardExit lives) and serious_python, with the platform packages recording the python-build re-pin and noting that no CPython or Pyodide versions move. serious_python_darwin's podspec and serious_python_android's build.gradle.kts carry their own version declarations alongside the pubspec and are bumped too.

Verification

Built a Flet app bundling matplotlib/numpy/Pillow against the published 20260908 and dart_bridge 1.9.0 artifacts, with the local cache cleared first so the real signed xcframework was downloaded:

$ nm -gU abc_1.app/Contents/Frameworks/dart_bridge.framework/Versions/A/dart_bridge | grep hard_exit
_serious_python_hard_exit
Check Result
Baseline, before any fix 2 crashes / 6 window closes
Window close, after 0 / 12, and 0 / 6 again on the published artifacts
sys.exit(3), after 0 / 5, exit code 3 preserved every run
SharedPreferences durability, both exit paths survives (0,1,2,3 across runs)
Older libdart_bridge (1.8.0) with this Dart side builds, runs, canHardExit false, falls back

At the observed baseline rate, twelve clean closes by chance is roughly 0.8%.

Caveat on the fallback: the soft lookup covers an older native libdart_bridge at runtime, which is verified above. It does not cover an older serious_python Dart package, where DartBridge.hardExit does not exist and a consumer pinning the template to 4.7.0 would fail to compile. That is inherent, but worth stating since it is narrower than "backwards compatible" suggests.

Not verified: the Windows and Linux paths. dart_bridge's TerminateProcess branch in particular is reasoned from DLL_PROCESS_DETACH running static destructors, not tested on a Windows host.

Chain

  1. dart-bridge 1.9.0 - released
  2. python-build 20260908 - released
  3. this PR
  4. flet - bumps its serious_python pin to 4.7.0 and uses hardExit in the build template

dart:io's exit() runs the normal C teardown, which destroys the C++ statics
inside every loaded CPython extension module. The interpreter lives on a
detached thread that may still be running, so that teardown can fault it,
giving a SIGSEGV on exit from an app that had already finished its work.
The reported case died in matplotlib's ft2font looking up a pybind11
type-caster map that had just been destructed.

hardExit routes to serious_python_hard_exit in dart_bridge 1.9.0, which
terminates without running atexit handlers or static destructors. The symbol
is resolved through the existing _lookupOrNull soft lookup, so an app built
against an older libdart_bridge still loads; canHardExit is then false and
the caller falls back to exit().

Version tables regenerated from python-build release 20260908, which carries
dart_bridge 1.9.0. No CPython or Pyodide versions change.
… versions

Every serious_python_* package gets a 4.7.0 entry, matching the convention
that each release is recorded in each package's own changelog. The
platform-interface entry carries the substance, since DartBridge.hardExit
lives there; the platform packages record the python-build re-pin and note
that no CPython or Pyodide versions move.

The darwin podspec and the android build.gradle.kts carry their own version
declarations alongside the pubspec, and both were still on 4.6.0.
@FeodorFitsner
FeodorFitsner merged commit eee9a1e into main Sep 9, 2026
65 of 67 checks passed
@FeodorFitsner
FeodorFitsner deleted the hard-exit branch September 9, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant