Link iOS example with rustc, and avoid C trampoline (#14780)

# Objective

On iOS:
- Allow `std` to do its runtime initialization.
- Avoid requiring the user to specify linked libraries and framework in
Xcode.
- Reduce the amount of work that `#[bevy_main]` does
- In the future we may also be able to eliminate the need for it on
Android, cc @daxpedda.

## Solution

We previously:
- Exposed an `extern "C" fn main_rs` entry point.
- Ran Cargo in a separate Xcode target as an external build system.
- Imported that as a dependency of `bevy_mobile_example.app`.
- Compiled a trampoline C file with Xcode that called `main_rs`.
- Linked that via. Xcode.

All of this is unnecessary; `rustc` is well capable of creating iOS
executables, the trick is just to place it at the correct location for
Xcode to understand it, namely `$TARGET_BUILD_DIR/$EXECUTABLE_PATH`
(places it in `bevy_mobile_example.app/bevy_mobile_example`).

Note: We might want to wait with the changes to `#[bevy_main]` until the
problem is resolved on Android too, to make the migration easier.

## Testing

Open the Xcode project, and build for an iOS target.

---

## Migration Guide

**If you have been building your application for iOS:**

Previously, the `#[bevy_main]` attribute created a `main_rs` entry point
that most Xcode templates were using to run your Rust code from C. This
was found to be unnecessary, as you can simply let Rust build your
application as a binary, and run that directly.

You have two options for dealing with this:

If you've added further C code and Xcode customizations, or it makes
sense for your use-case to continue link with Xcode, you can revert to
the old behaviour by adding `#[no_mangle] extern "C" main_rs() { main()
}` to your `main.rs`. Note that the old approach of linking a static
library prevents the Rust standard library from doing runtime
initialization, so certain functionality provided by `std` might be
unavailable (stack overflow handlers, stdout/stderr flushing and other
such functionality provided by the initialization routines).

The other, preferred option is to remove your "compile" and "link" build
phases, and instead replace it with a "run script" phase that invokes
`cargo build --bin ...`, and moves the built binary to the Xcode path
`$TARGET_BUILD_DIR/$EXECUTABLE_PATH`. An example of how to do this can
be viewed at [INSERT LINK TO UPDATED EXAMPLE PROJECT].
To make the debugging experience in Xcode nicer after this, you might
also want to consider either enabling `panic = "abort"` or to set a
breakpoint on the `rust_panic` symbol.

---------

Co-authored-by: François Mockers <mockersf@gmail.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
This commit is contained in:
Mads Marquart 2025-03-17 22:14:07 +01:00 committed by GitHub
parent 49659700b9
commit e6a6c9fb4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 52 additions and 206 deletions

View File

@ -428,7 +428,7 @@ jobs:
shell: bash
run: |
errors=""
for file in $(find examples tests -name '*.rs'); do
for file in $(find examples tests -name '*.rs' -not -path 'examples/mobile/*'); do
if grep -q "use bevy_" "$file"; then
errors+="ERROR: Detected internal Bevy import in $file\n"
fi

View File

@ -20,15 +20,6 @@ pub fn bevy_main(_attr: TokenStream, item: TokenStream) -> TokenStream {
main();
}
// SAFETY: `#[bevy_main]` should only be placed on a single `main` function
// TODO: Potentially make `bevy_main` and unsafe attribute as there is a safety
// guarantee required from the caller.
#[unsafe(no_mangle)]
#[cfg(target_os = "ios")]
extern "C" fn main_rs() {
main();
}
#[allow(unused)]
#input
})

View File

@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0"
[lib]
name = "bevy_mobile_example"
crate-type = ["staticlib", "cdylib"]
crate-type = ["lib", "cdylib"]
[dependencies]
bevy = { path = "../../" }

View File

@ -3,60 +3,21 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
134866208A035F8615C99114 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96A1E5B62F48B379829E8A0D /* Metal.framework */; };
2604C99FAB5A8322EDCABB9F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE71FBCAA714DB4F42459106 /* UIKit.framework */; };
442540D056ADB9AE61A0A590 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F1B41978FA53999AA836D0F /* Security.framework */; };
55892F1396056740E1AF9685 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AF7DE91055EBD05ED77E57F9 /* main.m */; };
55B7188F81C3C4183F81D3AE /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = A39528EB2CCB182F5328223A /* libc++.tbd */; };
57CD6306253C7A940098CD4A /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57CD6305253C7A940098CD4A /* AudioToolbox.framework */; };
57CD630E253C80EC0098CD4A /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 57CD630A253C7F5F0098CD4A /* assets */; };
6ADF1AB92CCDA73A00AF5F8E /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
26BF2C4863C966DABAB40DC8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8DBF1E2B5C613DA41701F6D9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D08AEBE0B1A9C9A7B8C7B33F;
remoteInfo = cargo_ios;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
160DB77300A3F1806F024D47 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = "<group>"; };
55EAC02897847195D2F44C15 /* bevy_mobile_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bevy_mobile_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
57CD6305253C7A940098CD4A /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
57CD630A253C7F5F0098CD4A /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../../assets; sourceTree = "<group>"; };
6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
8EE7F1E3B0303533925D7E33 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
96A1E5B62F48B379829E8A0D /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
9F1B41978FA53999AA836D0F /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
A39528EB2CCB182F5328223A /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
AF7DE91055EBD05ED77E57F9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
FE71FBCAA714DB4F42459106 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
D5A822CB2D6847BA8800BE4C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6ADF1AB92CCDA73A00AF5F8E /* QuartzCore.framework in Frameworks */,
442540D056ADB9AE61A0A590 /* Security.framework in Frameworks */,
134866208A035F8615C99114 /* Metal.framework in Frameworks */,
2604C99FAB5A8322EDCABB9F /* UIKit.framework in Frameworks */,
55B7188F81C3C4183F81D3AE /* libc++.tbd in Frameworks */,
57CD6306253C7A940098CD4A /* AudioToolbox.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
321F7D6A765B38E746C35105 /* Products */ = {
isa = PBXGroup;
@ -70,9 +31,7 @@
isa = PBXGroup;
children = (
57CD630A253C7F5F0098CD4A /* assets */,
160DB77300A3F1806F024D47 /* bindings.h */,
8EE7F1E3B0303533925D7E33 /* Info.plist */,
AF7DE91055EBD05ED77E57F9 /* main.m */,
);
path = "ios-src";
sourceTree = "<group>";
@ -89,49 +48,23 @@
EB028409C2D0655412DA6E44 /* Frameworks */ = {
isa = PBXGroup;
children = (
6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */,
57CD6305253C7A940098CD4A /* AudioToolbox.framework */,
A39528EB2CCB182F5328223A /* libc++.tbd */,
96A1E5B62F48B379829E8A0D /* Metal.framework */,
9F1B41978FA53999AA836D0F /* Security.framework */,
FE71FBCAA714DB4F42459106 /* UIKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXLegacyTarget section */
D08AEBE0B1A9C9A7B8C7B33F /* cargo_ios */ = {
isa = PBXLegacyTarget;
buildArgumentsString = build_rust_deps.sh;
buildConfigurationList = AA00A0CFDB11F37F2BA3FC2E /* Build configuration list for PBXLegacyTarget "cargo_ios" */;
buildPhases = (
FE045B3D04D57B713A565FF8 /* Sources */,
);
buildToolPath = /bin/sh;
buildWorkingDirectory = .;
dependencies = (
);
name = cargo_ios;
passBuildSettingsInEnvironment = 1;
productName = cargo_ios;
};
/* End PBXLegacyTarget section */
/* Begin PBXNativeTarget section */
3BDB8152E4962373181B4FE5 /* bevy_mobile_example */ = {
isa = PBXNativeTarget;
buildConfigurationList = E714A1AEAAE517C348B5BD27 /* Build configuration list for PBXNativeTarget "bevy_mobile_example" */;
buildPhases = (
9F13800790AD9DBC2BC0F116 /* Sources */,
D5A822CB2D6847BA8800BE4C /* Frameworks */,
ECFC92D02C6F14FA0095E5B1 /* Rust */,
57CD630D253C80E60098CD4A /* Resources */,
);
buildRules = (
);
dependencies = (
19D4B9C22ADC6705B5132B4C /* PBXTargetDependency */,
);
name = bevy_mobile_example;
productName = bevy_mobile_example;
@ -158,7 +91,6 @@
projectRoot = "";
targets = (
3BDB8152E4962373181B4FE5 /* bevy_mobile_example */,
D08AEBE0B1A9C9A7B8C7B33F /* cargo_ios */,
);
};
/* End PBXProject section */
@ -174,31 +106,29 @@
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
9F13800790AD9DBC2BC0F116 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
55892F1396056740E1AF9685 /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FE045B3D04D57B713A565FF8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
/* Begin PBXShellScriptBuildPhase section */
ECFC92D02C6F14FA0095E5B1 /* Rust */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)/build_rust_deps.sh",
);
name = Rust;
outputFileListPaths = (
);
outputPaths = (
$TARGET_BUILD_DIR/$EXECUTABLE_PATH,
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "./build_rust_deps.sh\n";
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
19D4B9C22ADC6705B5132B4C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D08AEBE0B1A9C9A7B8C7B33F /* cargo_ios */;
targetProxy = 26BF2C4863C966DABAB40DC8 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* End PBXShellScriptBuildPhase section */
/* Begin XCBuildConfiguration section */
4AD7BC6FDD56FF18FA6DA7D7 /* Debug */ = {
@ -271,33 +201,8 @@
CODE_SIGN_IDENTITY = "";
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"ios-src/",
);
INFOPLIST_FILE = "ios-src/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = (
"$(inherited)",
"../../target/aarch64-apple-ios/release",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = (
"$(inherited)",
"../../target/aarch64-apple-ios-sim/release",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = (
"$(inherited)",
"../../target/x86_64-apple-ios/release",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-lbevy_mobile_example",
"-lc++abi",
);
PRODUCT_BUNDLE_IDENTIFIER = org.bevyengine.example;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@ -357,19 +262,6 @@
};
name = Release;
};
8265913A25816D964A847F1B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.rust.cargo-ios";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
A2D5B73DD30D562B6F366526 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -379,52 +271,14 @@
CODE_SIGN_IDENTITY = "";
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"ios-src/",
);
INFOPLIST_FILE = "ios-src/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = (
"$(inherited)",
"../../target/aarch64-apple-ios/debug",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = (
"$(inherited)",
"../../target/aarch64-apple-ios-sim/debug",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = (
"$(inherited)",
"../../target/x86_64-apple-ios/debug",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-lbevy_mobile_example",
"-lc++abi",
);
PRODUCT_BUNDLE_IDENTIFIER = org.bevyengine.example;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
FEA9B18D9236F9F6DC6DF799 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.rust.cargo-ios";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -437,15 +291,6 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
AA00A0CFDB11F37F2BA3FC2E /* Build configuration list for PBXLegacyTarget "cargo_ios" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8265913A25816D964A847F1B /* Debug */,
FEA9B18D9236F9F6DC6DF799 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
E714A1AEAAE517C348B5BD27 /* Build configuration list for PBXNativeTarget "bevy_mobile_example" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -6,30 +6,35 @@ set -eux
PATH=$PATH:$HOME/.cargo/bin
PROFILE=debug
RELFLAG=
if [[ "$CONFIGURATION" != "Debug" ]]; then
PROFILE=release
RELFLAG=--release
fi
set -euvx
if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then
# Assume we're in Xcode, which means we're probably cross-compiling.
# In this case, we need to add an extra library search path for build scripts and proc-macros,
# which run on the host instead of the target.
# (macOS Big Sur does not have linkable libraries in /usr/lib/.)
export LIBRARY_PATH="${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}"
fi
# add homebrew bin path, as it's the most commonly used package manager on macOS
# this is needed for cmake on apple arm processors as it's not available by default
export PATH="$PATH:/opt/homebrew/bin"
# Make Cargo output cache files in Xcode's directories
export CARGO_TARGET_DIR="$DERIVED_FILE_DIR/cargo"
# Xcode places `/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin`
# at the front of the path, with makes the build fail with `ld: library 'System' not found`, upstream issue:
# <https://github.com/rust-lang/rust/issues/80817>.
#
# Work around it by resetting the path, so that we use the system `cc`.
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
IS_SIMULATOR=0
if [ "${LLVM_TARGET_TRIPLE_SUFFIX-}" = "-simulator" ]; then
IS_SIMULATOR=1
fi
EXECUTABLES=
for arch in $ARCHS; do
case "$arch" in
x86_64)
@ -40,16 +45,24 @@ for arch in $ARCHS; do
# Intel iOS simulator
export CFLAGS_x86_64_apple_ios="-target x86_64-apple-ios"
cargo rustc --crate-type staticlib --lib $RELFLAG --target x86_64-apple-ios
TARGET=x86_64-apple-ios
;;
arm64)
if [ $IS_SIMULATOR -eq 0 ]; then
# Hardware iOS targets
cargo rustc --crate-type staticlib --lib $RELFLAG --target aarch64-apple-ios
TARGET=aarch64-apple-ios
else
# M1 iOS simulator
cargo rustc --crate-type staticlib --lib $RELFLAG --target aarch64-apple-ios-sim
TARGET=aarch64-apple-ios-sim
fi
esac
cargo build $RELFLAG --target $TARGET --bin bevy_mobile_example
# Collect the executables
EXECUTABLES="$EXECUTABLES $DERIVED_FILE_DIR/cargo/$TARGET/$PROFILE/bevy_mobile_example"
done
# Combine executables, and place them at the output path excepted by Xcode
lipo -create -output "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" $EXECUTABLES

View File

@ -1 +0,0 @@
void main_rs(void);

View File

@ -1,6 +0,0 @@
#import "bindings.h"
int main() {
main_rs();
return 0;
}

View File

@ -9,9 +9,11 @@ use bevy::{
winit::WinitSettings,
};
// the `bevy_main` proc_macro generates the required boilerplate for iOS and Android
// the `bevy_main` proc_macro generates the required boilerplate for Android
#[bevy_main]
fn main() {
/// The entry point for the application. Is `pub` so that it can be used from
/// `main.rs`.
pub fn main() {
let mut app = App::new();
app.add_plugins(
DefaultPlugins

View File

@ -0,0 +1,2 @@
//! The entry point for iOS applications.
use bevy_mobile_example::main;