Rust on iOS with SDL2
A guide to setting up an iOS project using Rust and SDL2
Here's a guide to setting up an iOS project using Rust and SDL2.
EDIT: These instructions tend to rot over time due to changes in xcode and SDL2's project structure.
Initialize a git repo and get SDL2
Run through these commands to init a git repo and grab SDL2.
cd ~/dev/rust
mkdir ios-sdl2
cd ios-sdl2
git init
git submodule add --name SDL2 https://github.com/spurious/SDL-mirror.git SDL2
git commit -m "Initial commit"
Now's a good time to add some things to your .gitignore
/target
Cargo.lock
xcuserdata
Commit the .gitignore
git add .gitignore
git commit -m "Add gitignore file"
Build SDL2
Open SDL2's iOS xcode project
open -a xcode SDL2/Xcode/SDL/SDL.xcodeproj
Set up the build scheme like this and hit play:
EDIT: The screenshot is a bit old, the correct setting is Framework-iOS > Any iOS Device (arm64 armv7)
Once this finishes, you will have a libSDL2.a
file... somewhere. (xcode put mine somewhere in ~/Library/Developer/Xcode/DerivedData/HASHED_FOLDER/Build/Products/
- but that's fine. We'll pull it in later.
Include files are in SDL2/include
Setup the xcode project
xcode will be used to deploy your app to the device. There's an example project we can pull from SDL2 to help us get started
cp -r "SDL2/Xcode-iOS/Template/SDL iOS Application" xcode
We need to fix up a few names from the template. There's a placeholder ___PROJECTNAME___
used as both a filename and within some files. (That's PROJECTNAME surrounded by three underscores)
- Rename
___PROJECTNAME___.xcodeproj
- Find and replace
___PROJECTNAME___
inios-sdl2.xcodeproj/project.pbxproj
Here's a script to do that and commit the changes
cd xcode
mv ___PROJECTNAME___.xcodeproj ios-sdl2.xcodeproj
sed -i "" 's/___PROJECTNAME___/ios-sdl2/g' ios-sdl2.xcodeproj/project.pbxproj
cd ..
git add xcode
git commit -m "Add xcode project"
Link against SDL2
Now launch xcode (the project is in /xcode/ios-sdl2.xcodeproj
) and hit the play.
You should get an error like this:
SDL.h file not found
This is because we copy/pasted a template. It has paths that need to be fixed and will not build immediately.
Add SDL2 headers to the include path
- Open the files "tab" (see image below)
- Select the root node "ios-sdl2"
- Select Build Settings at the top
- Use the search below and to the right and find "User Header Search Paths"
- Double click and modify the entry to "../SDL2/include"
Fix Broken Reference to SDL2 Library
First, CLOSE ALL INSTANCES OF XCODE. We need to make absolutely sure the SDL2 project is not open. Otherwise the following steps will fail. Now reopen /xcode/ios-sdl2.xcodeproj
We should fix up the SDL.xcodeproj reference that's in the files list. Point it at SDL2/Xcode/SDL/SDL.xcodeproj
.
This does a couple things:
- Makes SDL2 source code available in your project
- Allows you to easily static link against the output of this library
Add SDL2 as a Link Dependency
We need to fix up library reference so that the project builds.
- Select ios-sdl2 project in the top left corner. This will bring up the project configuration if it isn't already showing
- Scroll down to the Frameworks, Libraries, and Embedded Content section. You may see libSDL2.a with an empty outline
- If this is the case, select it and remove it with the "-".
- Click "+" so we can add it back correctly.
- Select libSDL2.a from the libSDL-iOS target and click add
Now it should look like this...
Final steps for Running in Simulator
If you try to run it now, you'll likely have a few link errors. Fix this up by going back to the Frameworks, Libraries, and Embedded Content section. Add these frameworks. (Click plus and type in the text box to quickly find it)
- AVFoundation.framework
- Security.framework
EDIT: I didn't need to do this step at this point, but I needed to add Security.framework later! (Also, you'll follow this step later to add a rust static library...)
With link errors solved, we get a new error message:
The application's Info.plist does not contain CFBundleShortVersionString.
Ensure your bundle contains a CFBundleShortVersionString.
Open Info.plist and add it.
- Open the files "tab" if it isn't already open
- Select Info.plist
- Right click Information Property List
- Select "Add Row"
- Type in CFBundleShortVersionString and hit enter. It will likely change to "Bundle version string (short)" - that's fine
- Set the value to "0.1.0"
At this point when you hit play, the simulator should come up. SDL2 should load and start drawing!
Deploying to a device
To follow this step, you may need an apple developer account. I'm not going to cover this in detail but I'll just mention briefly a few things I set:
- In the project settings under General, you can pick a target version, like iOS 12.4
- In the project settings under Signing & Capabilities, select your team and set a bundle identifier
- When I switched to iOS 12.4, I needed to add Metal.framework
Finally, write some Rust!
Time to get a rust project up and running! The general approach is that we will create a C library, link it, and call into it. I highly recommend checking out this repo: https://github.com/thombles/dw2019rust. Most of the below steps are covered in detail there.
First thing you'll need to do is grab the rust toolchains for:
- Physical devices:
aarch64-apple-ios
- Simulators:
x86_64-apple-ios
rustup target add aarch64-apple-ios x86_64-apple-ios
Now create a rust library
cargo new --lib game-ios
Modify the .toml to create a library. I'll also add in sdl2 and a simple logging crate.
[lib]
crate-type = ["lib", "staticlib"]
[dependencies]
sdl2 = "0.34"
env_logger = "0.6"
log="0.4"
And modify lib.rs
#[no_mangle]
pub extern "C" fn run_the_game() {
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Info)
.init();
log::info!("Started!");
}
Finally, some rust code! Rather than a standard main function, we expose a C-ABI compatible function. The main.c in our xcode project can link against the library created by rust and call into it.
For convenience, create an example. This allows building and running the library outside of xcode. I added examples/run_the_game.rs with:
use game_ios::*;
fn main() {
run_the_game();
}
Add a workspace file (Cargo.toml
) at the root of the project
[workspace]
members = [
"game-ios",
]
This does two things:
- Allows you to run
cargo build
andcargo run --example run_the_game
in the base of the project - Puts the output folder ("target") in the base of the project
At this point your project structure should look like this:
Linking Rust from xcode
First, let's automate building the rust project from xcode. Here's a script that will work. For information about the "lipo" commands, see here: https://github.com/thombles/dw2019rust/blob/master/modules/02 - Cross-compiling for Xcode.md
set -e
RUST_PROJ="/Users/pmd/dev/rust/ios-sdl2" <-- EDIT TO MATCH YOUR PATH
PATH="$PATH:/Users/pmd/.cargo/bin" <-- EDIT TO MATCH YOUR PATH
cd "$RUST_PROJ"
cargo build --package game-ios --target aarch64-apple-ios --verbose
cargo build --package game-ios --target x86_64-apple-ios --verbose
lipo -create target/aarch64-apple-ios/debug/libgame_ios.a target/x86_64-apple-ios/debug/libgame_ios.a -output target/libgame_ios.a
We can add this to xcode.
- Open the project settings and under the "Build Phases" section
- Add a Run Script phase
- Drag it up to be earlier in the build
- Add the script. Be sure to update the paths!
More info on setting up build scripts here: https://github.com/thombles/dw2019rust/blob/master/modules/04 - Build automation.md
Errors you might see now:
library not found for -ISystem/linking with cc failed: exit code 1
: I didn't use to have this problem and hopefully there's a better solution... but running cargo build manually once fixed it for me- Missing symbol
_run_the_game
: follow the steps above "Final steps for Running in Simulator" to add the rust static library to your project. It's intarget/libgame_ios.a
This will likely fail with an error message about a missing symbol
Finally, we need to add the rust output directory to the library search path. This is in the same area we updated the include paths for SDL.
Starting the Rust Code
We're close! Update main.c
// Including SDL.h is required to let SDL hook main and set up the app delegate
#include "SDL.h"
#include <stdio.h>
extern void run_the_game();
int main(int argc, char *argv[])
{
run_the_game();
printf("run_the_game returned");
return 0;
}
If you get a linker error "Undefined symbol: _SecRandomCopyBytes", ensure Security.framework is included. (See steps above)
Now you should see in the output:
[2020-07-21T07:08:50Z INFO game_ios] Started!
run_the_game returned
One More Build Config Setting
Rust does not produce bitcode compatible with xcode LLVM. So to run on a physical device, it must be disabled. It's in the same area as where we modified header/library paths, but make sure to enable showing "All" settings. More info here: https://github.com/thombles/dw2019rust/blob/master/modules/02 - Cross-compiling for Xcode.md#1-use-a-physical-iphone-or-ipad
A More Visual Demo
This code will make the screen change colors and print events to the console
use sdl2::pixels::Color;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use std::time::Duration;
#[no_mangle]
pub extern "C" fn run_the_game() {
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Info)
.init();
log::info!("Started!");
do_run_the_game().unwrap();
}
pub fn do_run_the_game() -> Result<(), String> {
let sdl_context = sdl2::init()?;
let video_subsystem = sdl_context.video()?;
let window = video_subsystem.window("rust-sdl2 demo", 800, 600)
.position_centered()
.opengl()
.build()
.map_err(|e| e.to_string())?;
let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?;
canvas.set_draw_color(Color::RGB(255, 0, 255));
canvas.clear();
canvas.present();
let mut event_pump = sdl_context.event_pump()?;
let t0 = std::time::Instant::now();
'running: loop {
for event in event_pump.poll_iter() {
println!("{:?}", event);
match event {
Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => {
break 'running
},
_ => {}
}
}
let time = (std::time::Instant::now() - t0).as_secs_f32();
let fraction = (time.sin() + 1.0) / 2.0;
let color = (fraction * 255.0) as u8;
canvas.set_draw_color(Color::RGB(0, 0, color));
canvas.clear();
canvas.present();
::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 30));
}
Ok(())
}
Commit all the changes
git add .
git commit -m "Rust app works"
And the output is here: