/LLVM-On-iOS

Script to build LLVM and Clang projects for use in iOS app and example iOS app using LLVM to interpret C++ programs

Primary LanguageShell

LLVM on iOS

The goal of this project is to illustrate how to use LLVM + Clang to provide an iOS app with some scripting capability.

Edit the program screenshot Interpret the program screenshot

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:

  1. We change the main function name to clangInterpret since iOS app already has main function.

  2. 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.

  1. 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.

  1. For real iOS device: The implementation of llvm::sys::getProcessTriple() is currently bogus according to the implementation of JITTargetMachineBuilder::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.

Preparations

Before building the project, you need to either

  1. compile LLVM (see instructions down below); or
  2. download our prebuilt XCFramework (the file named LLVM.xcframework.tar.xz) from our releases, then cd to the repo folder and do
tar -xzf PATH_TO_DOWNLOADED_TAR_XZ             # e.g. ~/Downloads/LLVM.xcframework.tar.xz

Known Limitations

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:

Add #include non-existing header Compilation error was printed out

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.

Build LLVM for iOS

The tools we needs

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!

Build LLVM and co.

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.

Behind the Scene

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

  1. how language interoperability works; and
  2. how to configure your Xcode project to use it.

Swift-C++ interoperability

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's Data to Objective-C's NSData to C++'s buffer char* (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.

Configure iOS App Xcode Project

  1. Create a new iOS app project in Xcode and copy LLVM.xcframework to the project folder.

  2. 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.

  3. 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, say LLVMBridge.mm (here, we use the .mm extension for Objective-C++ since we do need C++ to implement our LLVMBridge class) and then change the Objective-C bridging header setting in the project file to tell Xcode that the Objective-C class defined in LLVMBridge.h should be exposed to Swift. Again, go to Build settings your project and search for bridg and you should find Objective-C Bridging Header under Swift Compiler - General. Set it to PROJECT_NAME/LLVMBridge.h or if you are using more than just LLVM, a header file of your choice (but that header should include LLVMBridge.h).

Note: Only Objective-C classes in Objective-C Bridging Header are visible to Swift!

Objective-C Bridging Header Setting

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.

  1. 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 to No.

Bitcode Setting

Now you are ready to make use of LLVM glory.