Running Racket CS on iOS
As of iOS 14.4, non-debugged builds (i.e. ones run outside of XCode) fail with a dynamic code signing error and there is no way to work around this at the moment.
A couple of weeks ago, I started working on getting Racket CS to compile and run on iOS and, with a lot of guidance from Matthew Flatt, I managed to get it working (with some caveats). Those changes have now been merged, so I figured I’d write another one of these guides while the information is still fresh in my head.
Compile Racket for macOS and for iOS
To build Racket for iOS, clone the Racket repository and follow the cross-compilation instructions under “racket/src/README.txt”. The easiest approach is to create a “build” directory under “racket/src”, then configure the build from within that directory by running
|
|
and then
|
|
to compile Racket and set up a distribution. After running this series of commands, you should end up with a cross-compiled Racket distribution at “racket/” inside the source repository. Additionally, under “racket/src/build/local/”, you’ll have a compiled version of Racket CS for your host machine. You’ll use that version of Racket to cross-compile Racket sources for iOS.
Cross-compile Racket modules for iOS
I added a section on how to cross-compile Racket modules to the “Inside Racket” docs so refer to that. In short, if you save the following module under “app.rkt” somewhere
|
|
then you can run
|
|
to produce “app.zo”, a binary object containing the cross-compiled code for that module and all of its dependencies.
Set up your XCode project
To link against and use Racket CS within an XCode project, copy “racketcs.h”, “racketcsboot.h” and “chezscheme.h” from “racket/include/” into a sub-directory of your project, then add that sub-directory to the “Header Search Paths” section under your project’s “Build Settings” tab.
Then, disable Bitcode from the same section.
Next, copy “libracketcs.a”, “petite.boot”, “scheme.boot” and “racket.boot” from “racket/lib” into a sub-directory of your project called “vendor/” and drag-and-drop the “vendor/” directory into your XCode project. Then, instruct XCode to link “libracketcs.a” and “libiconv.tbd” with your code from the “Build Phases” tab. You’ll have to add “libracketcs.a” to your project using the “Add Other…” sub-menu.
Next, add a new C source file called “vendor.c” and answer “yes” if
prompted to create a bridging header for Swift. I tend to re-name the
bridging header to plain “bridge.h” because I don’t like the name that
XCode generates by default. If you do this, you’ll have to update the
“Objective-C Bridging Header” setting in your “Build Settings” tab. From
“bridge.h”, include “vendor.h” and inside “vendor.h” add definitions for
racket_init
and echo
|
|
then, inside of vendor.c
, implement them
|
|
Take a look at the Inside Racket CS documentation for details on the
embedding interface of Racket CS. The gist of racket_init
is that
it takes the paths to “petite.boot”, “scheme.boot”, “racket.boot” and
“app.zo” as arguments in order to initialize Racket and then load the
“app.zo” module, which you can do from the AppDelegate
’s
application(_:didFinishLaunchingWithOptions:)
method:
|
|
Upon successful initialization, you should be able to call the Racket echo
function from Swift:
|
|
Compile and run the project on a device and you should see “Hello from Racket!” get printed in your debug console.
Some XCode gotchas
If you copy “vendor/” into your project instead of creating “folder references” when you drag-and-drop it, then code signing may fail with an ambiguous error.
Avoid using symbolic links for any of your resources (like the stuff in “vendor/”). Doing so makes copying the code over to the device fail with a “security” error that doesn’t mention the root problem at all.