Porting notes
What it takes to build React Native's C++ core somewhere it was not built before, and the handful of upstream facts that the build scripts depend on.
This is the file native/bootstrap.sh, scripts/react-native.pin and
.github/workflows/ci.yml point at. Everything here was arrived at by building
rather than by reading, and each section says what breaks if you do not do it.
Building against an installed React Native
ReactCxxPlatform is not in the npm package.
packages/react-native/package.json lists ReactCommon, ReactAndroid and
ReactApple under files, and not ReactCxxPlatform, so an installed React
Native does not carry the layer an out-of-tree C++ platform is built on, and
react-native run-linux --build used to need a source checkout. Every app has
an installed React Native and almost none has a checkout.
The upstream fix is one line, adding "ReactCxxPlatform" to that files
array. It costs about 1% of the package. Nothing in the existing issues and pull
requests mentioning ReactCxxPlatform raises the packaging question, and
nothing has been opened from here: a one-line packaging change with one obscure
consumer is easy to ignore, and this platform has no users yet to point at.
The workaround is to fetch the directory, not to vendor it. When React
Native is an installed package with no ReactCxxPlatform, bootstrap does a
sparse, blobless clone of that one directory at the tag matching the app's exact
version: about 3MB and four seconds, a smaller bargain than the folly and
Hermes downloads it already makes.
Fetching is forced rather than preferred. Carrying a copy in this package was
the obvious answer and cannot work: that layer tracks ReactCommon closely, and
main's copy fails against 0.87.1 on a ResizeObserver header that did not exist
and a ReactInstance::createJSCallInvoker that had not been added. One copy
cannot serve several React Natives, and a vendored copy would pin this package
to a single version the way react-native-windows pins to one exact nightly.
Fetching at the app's own version keeps the C++ and the JavaScript in step by
construction.
A second, smaller gap, which the build found and a path check did not:
ReactCommon/react/nativemodule/cputime ships its C++ but not its codegen spec,
which lives under src/private/testing/fantom/specs/ and is reasonably
excluded. The package therefore contains C++ that cannot be compiled from the
package, and the failure reads as a missing NativeCPUTimeCxxSpec template,
like a codegen bug rather than a packaging one. Nothing links that target, so
this platform does not build it.
Which React Natives are supported
supported-versions.json names, for each supported React Native, the Hermes to
build. Bootstrap reads it before downloading or compiling anything and refuses
an unsupported version in seconds, with a message saying what is supported and
where to look.
Today that is React Native 0.87.x, verified against 0.87.1, and main for
development.
A pinned triple rather than following the app, because following the app meant every combination was its own experiment. Adding a version is now a deliberate act: build against it, run both suites, add an entry. It cannot be done by widening a range, and that is the point; 0.83 through 0.86 were once described as "expected to work", which was a prediction wearing the clothes of support. They are listed as untested and refused.
ReactCxxPlatform still comes from the app's exact version rather than from the
table, and that is not an inconsistency: the table bounds which minors are
supported, and the fetch keeps the C++ and the JavaScript exact within one.
Hermes on Windows
Windows is listed among the platforms Meta publishes a prebuilt Hermes for, and the prebuilt does not exist. So Windows builds Hermes from source, which needs three things, one of them an upstream bug.
HERMES_ALLOW_BOOST_CONTEXT=0. Hermes' vendored boost::context throws
std::bad_alloc from its Windows stack allocator, and Hermes compiles with
exceptions off. The POSIX allocator does not, which is why this is Windows-only.
This is Hermes' own option for exactly that case, the one its ASAN and
Emscripten builds use, and the whole path is behind #if HERMES_USE_BOOST_CONTEXT, so it is a clean fallback rather than a hole.
HERMES_EMPTY_BASES on VM::Environment. Static Hermes asserts that the
C++ Environment and the C SHEnvironment are layout-identical. sizeof
matches and the offsets do not, because Environment multiply-inherits from
VariableSizeRuntimeCell and an empty base, llvh::TrailingObjects, and the
MSVC ABI does not collapse empty bases the way the Itanium ABI does.
The fix is one word and Hermes already has it. Support/Compiler.h defines
#define HERMES_EMPTY_BASES __declspec(empty_bases)
with a comment saying it is necessary for PointerBase alignment under
HERMESVM_CONTIGUOUS_HEAP, which is the mode this builds in, and the macro is
applied to nothing at all in the entire tree. Dead code that was presumably
attached to something once. Applying it to Environment clears the assertion.
Exceptions, for React Native's jsi. A POSIX assumption hiding inside a difference between two compilers' defaults, and the most interesting of the three.
jsi.cpp throws JSError. Hermes' cmake/modules/Hermes.cmake strips /EHsc
out of CMAKE_CXX_FLAGS wholesale to avoid a D9025 warning, then puts /EHsc
or /EHs-c- back on each of its own targets through
hermes_update_compile_flags(). React Native's jsi is not one of its own
targets: it arrives through JSI_DIR, never goes through that helper, and so
ends up with no /EH flag at all, CMake's default having been removed on
its behalf. clang with no flag defaults exceptions on; clang-cl with no flag
defaults them off. The strip is safe on every platform Hermes is tested on and
fatal on this one.
HERMES_ENABLE_EH=ON is the supported knob for Hermes' own targets, and it has
to be HERMES_ENABLE_RTTI=ON alongside it, API/hermes/CMakeLists.txt refuses
the mixed case outright. Worth knowing because the failure arrives as a
configure error about ICU much further down the log, which is collateral and
sends you looking in the wrong place.
For the jsi target the flag has to come from CMAKE_CXX_FLAGS instead, spelled
/EHs /EHc: exactly equivalent, and not the literal string their REPLACE
looks for.
One trap, recorded because it cost a full rebuild: -DCMAKE_CXX_FLAGS=...
replaces CMake's platform defaults rather than adding to them, and those
defaults are where /DWIN32 /D_WINDOWS come from. Hermes' OSCompat.h keys its
<unistd.h> include on _WINDOWS, so overriding the variable makes a Windows
build start looking for POSIX headers. Pass the defaults back with it.
clang-cl rather than cl
The two front ends fail differently on Hermes, and the difference decided the toolchain for the whole core build:
- cl stops at 260 of 415 on
__builtin_expectin Static Hermes'static_h.h: a GCC and Clang builtin MSVC does not have, and not something a flag can supply. - clang-cl has that builtin and gets past it, which is what makes the three fixes above sufficient rather than the first of a long list.
docs/DECISIONS.md also prefers clang wherever there is a choice, for
independent reasons.
Upstream portability bugs this build works around
Both are the same shape (a POSIX assumption in a header) and both are fixed from the command line rather than by editing React Native's tree:
M_PIis used seven times inreact/renderer/components/view/conversions.h, to convert degrees fortransform: rotate. It is a POSIX extension rather than standard C++, and MSVC's<cmath>defines it only behind_USE_MATH_DEFINES.- A missing
<cstdint>inHttpUtils.h, and another inReactCxxPlatform/react/runtime/ReactInstanceConfig.h, which this build force-includes around. The second appears only at React Native 0.86 on Linux: 0.87's copy of the header also includes<functional>and<memory>, which pull<cstdint>in on libstdc++, and libc++ supplies it on a Mac regardless.
docs/backlog/upstream.md carries these and the rest.