`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
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:
#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:
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=0Expected: 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:
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:
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:
#ifndef HERMESVM_SAMPLING_PROFILER_AVAILABLE
#define HERMESVM_SAMPLING_PROFILER_AVAILABLE @HERMESVM_SAMPLING_PROFILER_AVAILABLE@
#endifand include/hermes/VM/Profiler/SamplingProfilerDefs.h:
#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_AVAILABLEThree 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 routeHERMESVM_ALLOW_JITalready takes (lib/config/libhermesvm-config.h.in:37-39). - The
#ifndefis what keeps a tree that never asks unchanged, and the#undefbefore 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.)
Source: facebook/hermes