#2185·hermes

`static_h`: the sampling profiler has no build switch, so a target with no POSIX signal delivery has to stub `sigaction` / `sigemptyset` / `pthread_kill` for an instrument it cannot run

Author: altweenCreated Sep 17, 2026Updated Sep 17, 2026

HERMESVM_SAMPLING_PROFILER_AVAILABLE is decided entirely inside include/hermes/VM/Profiler/SamplingProfilerDefs.h, from one hardcoded predefine, and there is no CMake variable anywhere in the tree that can change the answer:

cpp
#if defined(__EMSCRIPTEN__)
#define HERMESVM_SAMPLING_PROFILER_AVAILABLE 0
#else // !defined(__EMSCRIPTEN__)

#define HERMESVM_SAMPLING_PROFILER_AVAILABLE 1

#if defined(_WINDOWS)
#define HERMESVM_SAMPLING_PROFILER_WINDOWS
#else // !defined(_WINDOWS)
#define HERMESVM_SAMPLING_PROFILER_POSIX
#endif // !defined(_WINDOWS)

#endif // !defined(__EMSCRIPTEN__)

Everything downstream is already guarded on that macro and would need no change. lib/VM/Profiler/SamplingProfiler.cpp:9 is #if HERMESVM_SAMPLING_PROFILER_AVAILABLE, and lib/VM/Profiler/SamplingProfilerPosix.cpp:10 is #if defined(HERMESVM_SAMPLING_PROFILER_POSIX). The only thing missing is a way for a build to answer 0.

Why that matters for a cross-compile

We are porting hermesvm_a to an AArch64 POSIX-like embedded target whose libc declares <signal.h> but whose application runtime delivers no POSIX signals at all. hermesvm_a therefore links with three undefined symbols — sigaction, sigemptyset and pthread_kill — all of them from SamplingProfilerPosix.cpp, for an instrument that cannot work on that target and that nothing in our embedding asks for.

With no switch, the only way to get a clean link is to write three stubs to keep the linker quiet about a feature nobody wanted — and a stub is a worse answer than not compiling the code, because a sigaction that returns -1 leaves SamplingProfiler present-and-broken rather than absent. "This platform has no sampling profiler" is a fact the build knows and the header cannot guess.

Measured, cross-building hermesvm_a for that target at 5cee10a: switching the profiler off removes 149 build edges and takes the archive from 17,557,286 bytes to 17,395,084 bytes — −162,202.

One detail that makes the ask narrower than it looks

Switching the profiler off does not remove every signal symbol. sigfillset and pthread_sigmask remain, and their caller is not the profiler at all — it is llvh's Process::SafelyCloseFileDescriptor (external/llvh/lib/Support/Unix/Process.inc:235 and :240), which blocks every signal around a close(). Those two are ours to deal with and we do. This report is only about the three that belong to an instrument that has no switch.

Repro

No unusual target needed — the absence of the switch is visible from a configure on any host:

bash
git clone https://github.com/facebook/hermes.git hermes-static
cd hermes-static
git checkout 5cee10abc93667ea5538caecaf0a457c66fa5bdc

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DHERMES_ENABLE_TEST_SUITE=OFF -DHERMES_ENABLE_NAPI=OFF \
  -DHERMESVM_SAMPLING_PROFILER_AVAILABLE=0

Expected: the profiler is not built. Actual: nothing in the project reads that variable (CMake reports it among the manually-specified variables that were not used), Profiler/SamplingProfilerPosix.cpp is compiled into the library anyway, and nm on the result still shows the three signal calls as undefined. Also:

bash
grep -rn HERMESVM_SAMPLING_PROFILER_AVAILABLE --include=CMakeLists.txt --include='*.cmake' --include='*.h.in' .

answers nothing.

Environment

Windows 11 Pro 10.0.26200, x86-64 build host
CMake 3.31.6-msvc6 (the one Visual Studio 2022 ships), Ninja 1.13.2
cross-compiling for aarch64, clang 21.1.8 with lld and libc++

The build-host details are incidental: nothing above depends on the compiler, only on the target having no POSIX signal delivery.

Proposed fix

The patch we are running, in three files. lib/CMakeLists.txt, immediately before the existing configure_file:

cmake
set(HERMESVM_SAMPLING_PROFILER_AVAILABLE 1 CACHE STRING
  "Build the sampling profiler: 1 (yes) or 0 (no)")

configure_file(config/libhermesvm-config.h.in config/libhermesvm-config.h)

lib/config/libhermesvm-config.h.in, beside the HERMESVM_ALLOW_JIT block that already does exactly this:

c
#ifndef HERMESVM_SAMPLING_PROFILER_AVAILABLE
#define HERMESVM_SAMPLING_PROFILER_AVAILABLE @HERMESVM_SAMPLING_PROFILER_AVAILABLE@
#endif

and include/hermes/VM/Profiler/SamplingProfilerDefs.h:

cpp
#include "libhermesvm-config.h"

#if defined(__EMSCRIPTEN__)
#undef HERMESVM_SAMPLING_PROFILER_AVAILABLE
#define HERMESVM_SAMPLING_PROFILER_AVAILABLE 0
#endif

#ifndef HERMESVM_SAMPLING_PROFILER_AVAILABLE
#define HERMESVM_SAMPLING_PROFILER_AVAILABLE 1
#endif

#if HERMESVM_SAMPLING_PROFILER_AVAILABLE

#if defined(_WINDOWS)
#define HERMESVM_SAMPLING_PROFILER_WINDOWS
#else // !defined(_WINDOWS)
#define HERMESVM_SAMPLING_PROFILER_POSIX
#endif // !defined(_WINDOWS)

#endif // HERMESVM_SAMPLING_PROFILER_AVAILABLE

Three things about that shape are deliberate:

  • The default is 1, so every existing build is byte-identical. A configure that does not ask the question gets exactly what it gets today.
  • The answer travels through the generated libhermesvm-config.h, which consumers already include, so the library and an embedder can never disagree about the value. That is the route HERMESVM_ALLOW_JIT already takes (lib/config/libhermesvm-config.h.in:37-39).
  • The #ifndef is what keeps a tree that never asks unchanged, and the #undef before the __EMSCRIPTEN__ 0 keeps that platform's existing answer authoritative over the cache variable rather than the other way round.

Happy to send this as a PR if the shape is wanted; it is three hunks.

Where this came from

Measured while cross-compiling Static Hermes as the JavaScript runtime of a native game host for an embedded AArch64 target. No file under the Hermes checkout is edited other than by this patch, and the runtime itself was correct throughout — this is only about the tree's build configuration. (The project it came from is private at the time of filing, so no links.)