The goal of this project is to illustrate how to use LLVM + Clang to provide an iOS app with some scripting capability.
For the eager reader, we provide a sample iOS app project which has NO license attached so feel free to do whatever you want with it.
In this app, we use Clang's C interpreter example located in examples/clang-interpreter/main.cpp
of Clang source code to interpret a simple C++ program and print out the output on the iOS app's user interface.
(The file was renamed to Interpreter.cpp
to fit in with iOS development style. Also note that the example was removed from LLVM 14 but it should be available prior.)
The code is pretty much copied verbatim except for some minor modifications, namely:
-
We change the
main
function name toclangInterpret
since iOS app already hasmain
function. -
We comment out the last line
// llvm::llvm_shutdown();
so that you can call clangInterpret
again in the app.
This line only makes sense in the original program because it was a one-shot command line program.
- We add a third parameter
llvm::raw_ostream &errorOutputStream
to clangInterpret
and replace all llvm::errs()
with errorOutputStream
so we can capture the compilation output and pass it back to the app front-end to display to the user.
- For real iOS device: The implementation of
llvm::sys::getProcessTriple()
is currently bogus according to the implementation ofJITTargetMachineBuilder::detectHost()
. So we need to add the appropriate conditional compilation directive#ifdef __aarch64__ ... #else ... #endif
to give it the correct triple.
In the latest version, you should be able to edit the program, interpret it and see the output in the app UI.
Before building the project, you need to either
- compile LLVM (see instructions down below); or
- download our prebuilt XCFramework (the file named
LLVM.xcframework.tar.xz
) from our releases, thencd
to the repo folder and do
tar -xzf PATH_TO_DOWNLOADED_TAR_XZ # e.g. ~/Downloads/LLVM.xcframework.tar.xz
For simulator, can only build Debug version only!
You can run the app on the Mac (thank to Mac Catalyst) and iOS simulator. Do NOT expect the app to work on real iPhone due to iOS security preventing Just-In-Time (JIT) Execution that the interpreter example was doing. By pulling out the device crash logs, the reason turns out to be the fact the code generated in-memory by LLVM/Clang wasn't signed and so the app was terminated with SIGTERM CODESIGN.
If there is compilation error, the error message was printed out instead of crashing as expected:
Note: It does work if one launches the app from Xcode though.
To make the app work on real iPhone untethered from Xcode, one possibility is to use compilation into binary, somehow sign it and use system(). Another possibility would be to use the slower LLVM bytecode interpreter instead of ORC JIT that the example was doing, as many existing terminal apps illustrated. Also, check out L* C++ on the App Store. I recommend reading LLVM Programmer's Manual before using LLVM API.
- Xcode: Download from app store.
- Note that we need the Xcode command line tools as well.
- CMake: See installation instruction to add to
$PATH
. - Ninja: Download the binary and add it to
$PATH
.
Except for Xcode, the other items can be easily installed with Homebrew:
brew install cmake ninja
WARNING: It has come to our attention that LLVM's CMake Build configuration have some dependency discovery that might be interfered by Homebrew. For example, LLVM depends on libz
that is both supplied by Xcode and Homebrew. Since we are building for iOS, we really want the Xcode version of the library. But CMake can discover the Homebrew version and uses it instead! So you might want to build on a pristine machine. Don't get yourself HomescrewedTM!
Apple has introduced XCFramework to allow packaging a library for multiple-platforms (iOS, Simulator, watchOS, macOS) and CPU architectures (x86_64, arm64) that could be easily added to a project so that we do not have to switch out the libraries when we build the app for different targets (e.g. testing the app on real iPhone arm64 vs on the simulator x86_64).
Our script build-llvm.sh provides functions to build LLVM and libffi dependency for several iOS platforms.
It also has a function create_xcframework
to produce an XCFramework from them.
The script assumes the various tools aforementioned are installed and asccessible in $PATH
.
At this repo root:
source build-llvm.sh
build_libffi iphoneos
build_llvm iphoneos # iphonesimulator maccatalyst iphonesimulator-arm64 maccatalyst-arm64
create-xcframework iphoneos # iphonesimulator maccatalyst
(If you are building for use in your own development machine, you can skip some platforms that you do not need. For example, an M1 Macs can skip on iphonesimulator maccatalyst
as those are meant for Intel Macs.)
Our prebuilt release are built with GitHub Actions.
These days, you probably want to write your app in Swift whereas LLVM library is written in C++ so we need to create a bridge to expose LLVM backend to your app Swift frontend. This could be accomplished via Objective-C as an intermediate language:
Swift <-> Objective-C <-> C++
So to understand how our Sample project works, you need to know
- how language interoperability works; and
- how to configure your Xcode project to use it.
To start, you might want to start with Anthony Nguyen's Using C++ in Objective-C iOS app: My first walk for a quick intro on how to make use of C++ in Objective-C. (Note that both C++ and Objective-C are extensions of C and reduces to C.) An easy read on Objective-C and Swift interoperability could be found in Understanding Objective-C and Swift interoperability by RDerik. Combining these two articles is the basis for our Sample app.
A typical approach to allow C++ in Swift-based iOS app will be using
- Swift : Anything iOS-related (UI, file system access, Internet, ...)
- Objective-C : Simple classes (like
LLVMBridge
in our Sample app) to expose service written in C++. The main role is to convert data types between C++ and Swift. For example: Swift'sData
to Objective-C'sNSData
to C++'s bufferchar*
(and length). - C++ : Actual implementation of processing functionality.
Tip: When writing bridging classes, you should use NSData
for arguments instead of NSString
and leave the String <-> Data
conversion to Swift since you will want a char*
in C++ anyway.
Apple's Programming with Objective-C
is fairly useful in helping us write the Objective-C bridging class LLVMBridge
: Once we pass to C++, we are in our home turf.
-
Create a new iOS app project in Xcode and copy
LLVM.xcframework
to the project folder. -
In Xcode, add
LLVM.xcframework
to the project's Framework and Libraries. Choose Do not embed so that the static library is linked into the app and the entire framework is NOT copied to the app bundle. -
To create the Objective-C bridge between Swift and C++ mentioned at the beginning, add to your project a new header file, say
LLVMBridge.h
and an implementation file, sayLLVMBridge.mm
(here, we use the.mm
extension for Objective-C++ since we do need C++ to implement ourLLVMBridge
class) and then change the Objective-C bridging header setting in the project file to tell Xcode that the Objective-C class defined inLLVMBridge.h
should be exposed to Swift. Again, go to Build settings your project and search forbridg
and you should find Objective-C Bridging Header under Swift Compiler - General. Set it toPROJECT_NAME/LLVMBridge.h
or if you are using more than just LLVM, a header file of your choice (but that header should includeLLVMBridge.h
).
Note: Only Objective-C classes in Objective-C Bridging Header are visible to Swift!
At this point, we should be able to run the project on iOS simulator. To build the app for real iOS devices, an extra step is needed.
- Since we are using a bunch of precompiled static libraries (and not the actual C++ source code in our app), we need to disable bitcode. Search for
bitcod
and set Enable Bitcode setting toNo
.
Now you are ready to make use of LLVM glory.