Skip to content

cgen: emit GC_allow_register_threads() constructor for shared libs#27200

Open
eptx wants to merge 1 commit into
vlang:masterfrom
cx-home:fix-shared-libgc-init-pr
Open

cgen: emit GC_allow_register_threads() constructor for shared libs#27200
eptx wants to merge 1 commit into
vlang:masterfrom
cx-home:fix-shared-libgc-init-pr

Conversation

@eptx
Copy link
Copy Markdown

@eptx eptx commented May 19, 2026

Closes #27178.

When a V -shared library is dlopen'd by a host runtime that spawns
its own OS threads (Rust cargo workers, C# tasks, Java JNI threads,
etc.), the host threads that enter V code must be registered with
libgc — otherwise libgc aborts with "Collecting from unknown thread"
on the first collection triggered from an unregistered thread.

Per-thread registration via GC_register_my_thread() is inherently
caller-side, but the process-level enable GC_allow_register_threads()
can be done by V itself at library load time. This PR emits a
__attribute__((constructor)) that calls it, so bindings no longer
have to remember.

The constructor also calls GC_set_pages_executable(0) before
GC_INIT() to disable libgc's trampoline allocator (which requests
rwx pages via mprotect — denied under macOS hardened runtime). The
explicit GC_INIT() makes the timing deterministic so subsequent
allocations don't lazy-init under different settings.

All three calls are:

  • Gated on gc_mode in [boehm_full, boehm_incr, boehm_full_opt, boehm_incr_opt, boehm_leak]
  • Skipped on Windows (which uses a different shared-lib init path via gen_windows_shared_library_boehm_init)
  • Guarded by #if defined(GC_THREADS) for the thread-registration call

Tested locally on macOS 26.4 ARM64 against a real-world V -shared
consumer (the cx-home/cx project, a structured-data library with 5
host-language bindings). Before this PR, every Rust/Go/C#/Java/TS
binding had to hand-roll a cx_init wrapper that called
GC_allow_register_threads() at module load. After: bindings can
just dlopen the library and start using it.

V's own make test vlib/builtin passes 35/35 with this patch.

…lang#27178)

When a V `-shared` library is dlopen'd by a host runtime that spawns
its own OS threads (Rust cargo workers, C# tasks, Java JNI threads,
etc.), the host threads that enter V code must be registered with
libgc — otherwise libgc aborts with "Collecting from unknown thread"
on the first collection triggered from an unregistered thread.

Per-thread registration is inherently caller-side (the binding owns
when it spawns), but the process-level enable
(`GC_allow_register_threads()`) can be done by V itself at library
load time. This commit emits a separate `__attribute__((constructor))`
that calls it, so the binding no longer has to remember.

The constructor also calls `GC_set_pages_executable(0)` before
`GC_INIT()` to disable libgc's trampoline allocator (which requests
rwx pages via mprotect — denied under macOS hardened runtime). The
explicit `GC_INIT()` makes the timing deterministic so subsequent
allocations don't lazy-init under different settings.

Both calls are gated on `gc_mode in [boehm_full, boehm_incr,
boehm_full_opt, boehm_incr_opt, boehm_leak]` and skipped on Windows
(which uses a different shared-lib init path via
gen_windows_shared_library_boehm_init).

Closes vlang#27178.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1f65906ef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vlib/v/gen/c/cgen.v
Comment on lines +9754 to +9755
g.writeln('__attribute__ ((constructor))')
g.writeln('static void _v_shared_lib_gc_init(void) {')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Assign constructor priority for GC init

The new _v_shared_lib_gc_init and existing _vinit_caller are both emitted as plain __attribute__((constructor)) functions, but no priority is specified, so their relative execution order is not guaranteed by the toolchain. If _vinit_caller runs first, _vinit() can touch GC before GC_set_pages_executable(0) / GC_allow_register_threads() / GC_INIT() run, which defeats the fix and can still surface hardened-runtime or "unknown thread" failures in shared-library hosts. Use explicit constructor priorities (or call GC init from _vinit_caller before _vinit) to enforce ordering.

Useful? React with 👍 / 👎.

@SurmanPP
Copy link
Copy Markdown
Contributor

It already is emitted but into the main function. It would make more sense to just move it into the _vinit() function.

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.

-shared: auto-call GC_allow_register_threads() from a library constructor

2 participants