Running Racket BC on iOS

As of 2021-01-18, it is possible to run Racket CS on iOS.

/u/myfreeweb pointed out to me in a lobste.rs thread yesterday that Racket compiles just fine on aarch64 and that led me down a rabbit hole trying to get Racket running inside an iOS application. I finally succeeded so I figured I'd write down my findings in hopes of helping future Racketeers (myself included) going down this path!

Compile Racket for macOS

A recent-enough version of Racket is required in order to compile Racket for iOS. The best way to do that is to clone the Racket repo and follow the build instructions which should be as simple as running make in the repository root.

Assuming you're following along in a terminal session, run

export RACKET_SRC=$(pwd)

You'll need to reference this directory in the following steps.

Compile Racket for iOS

Once you've successfully compiled Racket for macOS, clone the Racket repository again, this time under a different directory. I called this directory racket-ios to differentiate the two, but you can call it whatever. Make sure the same commit is checked out in both repos and run through the following build steps starting at the repository root:

mkdir racket/src/build \
  && cd racket/src/build \
  && ../configure \
        --host=aarch64-apple-darwin \
        --enable-ios=iPhoneOS \
        --enable-racket="$RACKET_SRC/racket/bin/racket"

This will configure the build to create objects that can run on a physical device. To build Racket for the simulator instead, change the host to x86_64-apple-darwin and enable-ios from iPhoneOS to iPhoneSimulator. For details on these flags, see the cross compiling instructions in the Racket repo.

~~Although the instructions currently don't mention that pthread support is required when configuring the build, the code will fail to compile without it.~~ Matthew Flatt pushed a fix for this today!

Next, run make cgc && make install-cgc to compile the code and the packages. This builds the conservative GC variant of Racket. I started out trying to get everything running using the 3m variant of Racket (with a precise GC), but I ran into a number of roadblocks, including an LLVM bug from 2015 so I eventually gave up and switched to the CGC variant.

Create the Xcode Project

Create a new iOS-based project in Xcode. Inside that project, add a new group called "Frameworks" and then drag and drop racket/libmzgc.a, racket/libracket.a and rktio/librktio.a from the racket/src/build directory into the "Frameworks" group. Make sure "Copy items if needed" is toggled.

Drag n' Drop

Open the project settings and, from the "Build Phases" -> "Link Binary with Libraries" section, add libiconv.tbd. Racket depends on this library.

Link iconv

Copy racket/include into your project and then from the "Build Settings" -> "Search Paths" -> "Header Search Paths" section add $(SRCROOT)/include.

Headers

Add a new header file called BridgingHeader.h and then from the "Build Settings" -> "Swift Compiler - General" -> "Objective-C Bridging Header" add the path to the file. Everything defined in this file will be made available to the Swift code. Inside the file add:

#include "Interop.h"

Create a new C file called Interop.c and add

static int run(Scheme_Env *e, int argc, char *argv[]) {
    return 0;
}

void run_racket() {
    scheme_main_setup(1, run, 0, NULL);
}

In its associated header file, Interop.h, add

#include "scheme.h"

void run_racket(void);

Finally, inside the AppDelegate's application:didFinishLaunchingWithOptions method, add a call to run_racket and run your project on your device to ensure everything compiles and runs properly.

If everything runs correctly, then pat yourself on the back, you've just run Racket on iOS! Of course, Racket's not really doing much at this point. Let's embed a Racket module into the project.

In the project source directory, create a new file called hello.rkt with the following contents:

#lang racket/base

(displayln "Hello, World!")

From the command line, compile hello.rkt and all its transitive dependencies into a C file:

"$RACKET_SRC/racket/bin/raco" ctool --c-mods hello.c hello.rkt

Next, update Interop.c to include the resulting C file:

#include "hello.c"

And then update run in that same file to initialize and run that module:

static int run(Scheme_Env *e, int argc, char *argv[]) {
    Scheme_Object *a[2];
    declare_modules(e);
    a[0] = scheme_make_pair(scheme_intern_symbol("quote"),
                            scheme_make_pair(scheme_intern_symbol("hello"),
                                             scheme_make_null()));
    a[1] = scheme_false;
    scheme_dynamic_require(2, a);
    return 0;
}

Note that the name of the module passed to scheme_intern_symbol is "hello", the same as the file name.

Run your project, and you should see "Hello, World!" in your console.

It took a little bit of work, but we got there! From here, you should be able to do more interesting stuff. I'll get into some of that when I start porting Remember to iOS. In the mean time, you can read up on this stuff over here!