Skip to content

Quiet panic hook for generated wrappers, owned by code that outlives the artifact #304

Description

@terasakisatoshi

Split out of #302, which quieted the deliberate panics of the hot-reload stress crate and deliberately left the generated wrappers alone.

Why

A caught panic is not an incident: the generated extern "C" wrapper records the message in its channel and Julia raises RustCall.RustPanicError (#244). Rust's default hook runs first, though, and prints thread '<unnamed>' panicked at … to stderr before the unwind ever reaches catch_unwind. Every panic that RustCall handles correctly still looks like a crash in the log.

The scale is small but the shape is wrong: after #302, Pkg.test() shows 24 such lines, all from #[julia] wrappers doing exactly what they are supposed to do.

Why it is not a one-liner

A first attempt (in #302, reverted there) gave each wrapper a Once-guarded hook that stays silent while the thread is inside that wrapper's boundary and otherwise delegates to the previous hook. Review found two defects, both real:

  1. A hook closure installed from a cdylib can outlive the cdylib. With the default static std, each image owns its own hook registry and closing the image drops both together. With RUSTFLAGS=-C prefer-dynamic — which RustCall tracks in ArtifactId and supports — std and its hook registry are shared, so after unload_library(name; close = true) the registry holds a callback into unmapped code. Any later panic anywhere in the process jumps into freed memory. This matters most on Windows, where the repository requires libraries to be unloaded before their temporary trees can be removed.
  2. Per-wrapper Once values do not serialize take_hook/set_hook. Two first calls to different wrappers of one library can both take the default hook before either sets its replacement; the last setter wins and the other wrapper's hook is silently dropped. The pair also briefly exposes the default hook to already-initialised wrappers.

Design

  • One hook per library, not per wrapper. A single OnceLock guarding one take_hook/set_hook pair; the wrappers only bump an "inside the boundary" thread-local counter. This is the part that needs a decision: #[julia] expands one item at a time and has no crate-wide state to hang a shared item on — the same constraint that made the panic channel per wrapper (see the comment on panic_channel in deps/rustcall_core/src/codegen.rs). The inline path (expand.rs, which emits a whole file) can emit one; the proc-macro path cannot without a convention the user has to follow.
  • An uninstall entry point. Export rustcall_uninstall_panic_hook(), which restores the hook that was taken. Resolve it eagerly at load time, exactly as the panic channel is, and have the loader call it through the generation snapshot before dlclose — in close_artifact_handle! / the retire-close path — skipping it when the symbol is absent, so artifacts built by an older RustCall still close.
  • Do not install the hook at all when the build environment contains prefer-dynamic. ArtifactId already records RUSTFLAGS, so the decision is available at generation time. Document the exception in docs/src/panics.md.

Acceptance criteria

  • A #[julia] function that panics produces no panicked at line on stderr, and still raises RustCall.RustPanicError with the message
  • After unload_library(name; close = true), a panic in another library prints through the restored hook — asserted on captured stderr, not inferred
  • Two wrappers of one library, first called concurrently from two threads, both end up quiet
  • An artifact built with -C prefer-dynamic keeps the default hook, and says so in docs/src/panics.md
  • The golden corpus is regenerated and reviewed

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bqtry6ehnv5YzeEdrTPCTD

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:codegenrustcall_core, proc-macro, extractor, manifestarea:loadingdlopen, hot reload, generations, registries, DLL lockingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions