fix(ext/napi): polyfill libuv thread + semaphore primitives (#34571)
## Summary
Follow-up to #34488, which added the first tier of libuv polyfills so
native
addons that link against libuv directly (e.g. `@sentry/profiling-node`)
can
resolve their `uv_*` undefined symbols at `dlopen` time.
That PR's review asked to verify which of the open
[`node native
extension`](https://github.com/denoland/deno/issues?q=state%3Aopen%20label%3A%22node%20native%20extension%22)
issues it fixed or moved forward. The most common *remaining* failure
mode for
that class of addon is libuv's portable **threading and
synchronization**
primitives (`uv_thread_*`, `uv_sem_*`) — addons use them for their own
background work instead of the raw OS APIs, and on macOS a missing one
surfaces
as the infamous `dyld: missing symbol called`.
This PR adds those primitives to the compat layer:
- `uv_thread_create` / `uv_thread_join` / `uv_thread_self` /
`uv_thread_equal`
— backed by `std::thread`. We hand out a `u64` token (stored in the
addon-allocated `uv_thread_t`, which is always pointer-sized) and keep
the
real `JoinHandle` in a process-global registry, so `uv_thread_self` /
`uv_thread_equal` stay consistent and we never depend on the platform
`pthread_t` / `HANDLE` layout.
- `uv_sem_init` / `uv_sem_destroy` / `uv_sem_post` / `uv_sem_wait` /
`uv_sem_trywait` — a counting semaphore built on `parking_lot`.
`uv_sem_t` is
as small as a 4-byte mach `semaphore_t` on macOS (vs a 32-byte `sem_t`
on
Linux), so we store only a `u32` token in the addon struct and keep the
state
in a registry too. The registry lock is held only to look the semaphore
up,
never while blocking in `uv_sem_wait`.
These primitives are self-contained — they never touch the event loop —
so the
polyfills are exact, unlike the loop-bound handles which have to be
bridged onto
`deno_core`'s `uv_compat` layer.
### Verification of the `node native extension` issues vs #34488
| Issue | Failure | Status after #34488 |
| --- | --- | --- |
| #28621 zeromq v6 | `undefined symbol: uv_unref` | **Fixed** — #34488
added `uv_unref` |
| #26421 @sentry/profiling-node | `dyld missing symbol called` |
**Partial** — libuv half fixed; V8 C++ ABI half still blocked (out of
scope, needs a `rusty_v8` change) |
| #26656 node-rdkafka, #29152 node-pty | `dyld missing symbol called` |
**Class addressed here** — these reach for libuv threading primitives
this PR adds |
| #18345 zeromq v5 | `napi_register_module_v1 not found` | Unrelated
(NAN/node-gyp addon registration) |
| #16578 sharp, #24395 oracledb | module resolution / wrong arch `.node`
| Unrelated to uv symbols |
| #23465, #17411, #16642, #25956 | `LoadLibraryExW failed` (Windows DLL
deps) | Unrelated to uv symbols |
| #29203 @duckdb/node-api | `deno compile` `@rpath/libduckdb.dylib` not
found | Unrelated to uv symbols |
## Test plan
- [x] `cargo check -p deno_napi -p test_napi`
- [x] `cargo clippy -p deno_napi -p test_napi --all-targets`
- [x] `cargo test -p deno_napi` — new `uv::tests::thread_and_semaphore`
unit test passes (a worker thread posts a counting semaphore three
times;
the main thread drains it, joins the worker, and checks `uv_sem_trywait`
/
`uv_thread_equal`), alongside the existing `sizes` test.
- [x] New `napi uv thread + semaphore` integration test (`tests/napi`) —
a worker thread increments a
counter and posts a counting semaphore three times; the main thread
drains
the semaphore, joins the worker, and asserts the results. Exercises
every new
symbol, resolved from the host `deno` binary at runtime.
- [x] Existing `ext/napi` uv tests still pass.
- [ ] CI
Refs #34488
Closes denoland/orchid#333
---------
Co-authored-by: divybot <divybot@users.noreply.github.com>
Co-authored-by: Divy Srivastava <me@littledivy.com>