Technical Guide, Part 1: Compiling Hermes for Apple Platforms
Months of our intense work with teams at Facebook and Microsoft resulted in bringing Hermes to iOS. We are happy to share the details of the process in a series of articles. This article is the third in the series and the first to focus on the technical journey:
- Bringing Hermes to iOS in React Native 0.64
- Hermes Performance on iOS: How it Compares with JSC
- Technical Guide, part 1: Compiling Hermes Engine for Apple Platforms (you are here)
- Technical Guide, part 2: Integrating Hermes with React Native
You are going to find out how we brought Hermes to iOS and how you can implement it yourself. We provide a detailed guide to Hermes implementation based on the actual work done. So, if you want to learn more about how different core pieces play together, keep reading!
Compiling Hermes for Apple platforms
Before we talk about bringing Hermes to React Native on iOS, we actually need to compile it for Apple platforms. Hermes is written in C++ and compiled with cmake to existing platforms, so at a glance, it sounds like fun!
Just to be on the safe side, let me explain that C++ is one of these cross-platform languages that can run literally everywhere. For example, you can write native modules in C++ for Android and on iOS (hey, the Objective-C is not just similar in its name). Thanks to that, seeing a task of compiling Hermes on Apple devices didn’t sound that scary when I first started playing around that topic.
Thankfully, I didn’t have to start from the middle of nowhere (but I have to admit that playing around with cmake in general was quite an experience!). Folks at Microsoft have been working on bringing Hermes to Mac for their React Native macOS project. The work was done primarily by Eloy Durán (@alloy), who sent a PR to Hermes with the base for my work.
On a high level, this PR enables cmake to package Hermes in a dynamic library so that it can be used on a macOS platform. To make the integration with Apple ecosystem smoother, the PR adds a special Podspec so that you don’t have to manually import a framework file to your project. You can let CocoaPods do that magic for you instead.
At this point, I was amazed by the comprehensiveness of cmake and the number of out-of-the-box features it provides. If you look at the changes in the aforementioned PR, they’re all related to the build system. It’s mind blowing to see that such an advanced project like a JavaScript engine can be run on macOS by just flipping a few flags, i.e. without changing the business logic of the engine itself.
That’s good for me and all of you planning to work on C++ bits in the future! With that in mind, let’s move onto the iOS part.
On the way to iOS
Having Hermes running on macOS was a good indicator that it might work on iOS as well. In case you want a quick version — here’s my PR with all the changes. If you’re curious about all the steps and a bit of technical explanations, carry on.
#1
First thing I had to do was to tell cmake that it is no longer building Hermes for macOS, but for iOS. This can be achieved by setting a special variable CMAKE_OSX_SYSROOT to configure the build pipeline to target specific SDK.
1 set(CMAKE_OSX_SYSROOT ${HERMES_APPLE_TARGET_PLATFORM})
I ended up going straight with a variable. We will need to build Hermes for every platform and architecture separately, which means building it a couple of times. Having a variable definitely helps — we can change its value depending on what we are targeting.
The list of all platforms and architectures should be aligned with what React Native supports right now — otherwise, developers may run into issues on certain devices.
Here’s a breakdown of the platforms together with their architectures.
#2
Another important thing was to tell cmake where to actually output generated files for every platform.
By default, the library would be placed under a Library/Frameworks/hermes.framework path within a build folder. Unfortunately, that would result in one build process overwriting the artifacts from the previous one.
Since I wanted to keep the artifacts for every platform, I ended up tweaking the location where the files are placed:
1 install(DIRECTORY ${DSYM_PATH} DESTINATION
2 Library/Frameworks/${HERMES_APPLE_TARGET_PLATFORM})
As a result, the files would be now placed under Library/Frameworks/iphonesimulator or Library/Frameworks/iphoneos, depending on whether we’re building for a device or a simulator.
#3
Now that the platform part was sorted, it was time to look at the architectures. The idea was to precompile Hermes in all possible configurations so that you don’t have to run it from source. That would be not only quite a time consuming process, but also prone to many errors, due to different configurations of our development machines.
To do so, for each invocation of cmake, I ended up setting CMAKE_OSX_ARCHITECTURES with the right value for every platform. Looking at the table I have shared just a few paragraphs earlier, that would be “armv7;armv7s;arm64” for iPhone and “x86_64;i386” for iPhone Simulator.
Since that variable can be passed as a command line argument straight to cmake, there is no custom code that I had to do to make it work.
#4
The last thing to set was the deployment target — the version that we are targeting and is the minimum supported by Hermes. Again, that one is supported by cmake out of the box, so no changes here.
The value of CMAKE_OSX_DEPLOYMENT_TARGET was set equally to “10.0” for both simulator and the device.
build_apple_framework
After testing the combinations a few times, I packaged them in a helper Bash function, called build_apple_framework, that takes these settings and tells CMake what to do.
1 build_apple_framework “iphoneos” “armv7;armv7s;arm64” “10.0”
2 build_apple_framework “iphonesimulator” “x86_64;i386” “10.0”
Thanks to that, it becomes trivial to control what platforms and architectures Hermes supports on iOS.
Bonus points: it can be used to build macOS version too, so I went ahead and updated @alloy part too:
1 build_apple_framework “macosx” “x86_64” “10.0”
hermes.framework files
After building Hermes with CMake for all the combinations, I ended up with two hermes.framework files: for iPhone supporting armv7, armv7s and arm64 as well as for iPhone Simulator supporting x86_64 and i386.
It would be a poor developer experience if you had to change a hermes.framework in your project depending on whether you run on a device or a simulator. It would definitely hinder your work if you had to manually replace the library in your project.
Thankfully, there are universal frameworks, in other words — frameworks that support more than a single platform. Simply put — it’s a way to combine two hermes.framework into a single one!
You can create one programmatically with a lipo — a tool to create multi-architectural files. To generate a universal framework file, the invocation would look as follows:
lipo -create -output
1 Library/Frameworks/iphoneos/hermes.framework/hermes
2 Library/Frameworks/iphoneos/hermes.framework/hermes
3 Library/Frameworks/iphonesimulator/hermes.framework/hermes
To speed things up, I decided to merge all additional architectures into the iPhone binary. The first argument to lipo is the destination, the following ones are input binaries that should be combined together.
Just like before, I moved the logic into a Bash function, called create_universal_framework:
1 create_universal_framework “iphoneos” “iphonesimulator”
Again, such an approach allows us to easily control the contents of the final hermes.framework file.
Last but not least
The last piece was to update the Hermes.podspec created by @alloy to add iOS support.
That required changing spec.vendored_frameworks to spec.osx.vendored_frameworks and spec.ios.vendored_frameworks to tell CocoaPods that this package contains frameworks for both macOS as well as iOS (note that macOS and iOS binaries can’t be merged into a single universal framework — they are separate).
In other words, replacing this:
1 spec.vendored_frameworks = “destroot/Library/Frameworks/hermes.framework”
with:
1 spec.ios.vendored_frameworks = “destroot/Library/Frameworks/iphoneos/hermes.framework”
2 spec.osx.vendored_frameworks = “destroot/Library/Frameworks/macosx/hermes.framework”
Try Hermes yourself
The process of doing CMake reverse engineering took me three weeks, but it was worth it. I have learned a lot about build tools and this knowledge will be very useful in the future.
You should definitely clone Hermes and play around with it. Follow our Hermes implementation guide and test it yourself. It’s quite easy to get started and working on a JavaScript engine can get really rewarding!
If you want to learn more about Hermes, check our podcast: React Native 0.64 with Hermes for iOS. My guests, Microsoft and Facebook engineers, discuss the engine in detail!
What’s next?
In the next part of this guide, “Integrating Hermes with React Native,” we will go through the steps that are needed to enable a custom engine to work with React Native, instead of the default JSC.
This article was originally published at callstack.com on May 26, 2021.