Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS Example: Fix the build to make it work again #1437

Merged
merged 1 commit into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ members = [
"examples/sprites",
"examples/todolist",
"examples/custom-types",
"examples/app/uniffi-bindgen-cli",

"fixtures/coverall",
"fixtures/callbacks",
Expand Down
14 changes: 9 additions & 5 deletions examples/app/ios/IOSApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"$(SRCROOT)/Generated/$(INPUT_FILE_BASE)FFI.h",
"$(SRCROOT)/Generated/$(INPUT_FILE_BASE).swift",
);
script = "# Generate swift bindings for the todolist rust library.\necho \"Generating files for $INPUT_FILE_PATH\"\n\"$SRCROOT/../../../target/debug/uniffi-bindgen\" generate \"$INPUT_FILE_PATH\" --language swift --out-dir \"$SRCROOT/Generated\"\necho \"Generated files for $INPUT_FILE_BASE in $SRCROOT/Generated\"\n";
script = "# Generate swift bindings for the todolist rust library.\necho \"Generating files for $INPUT_FILE_PATH\"\n$HOME/.cargo/bin/cargo run -p uniffi-bindgen-cli -- \\\n generate \"$INPUT_FILE_PATH\" \\\n --language swift \\\n --out-dir \"$SRCROOT/Generated\"\necho \"Generated files for $INPUT_FILE_BASE in $SRCROOT/Generated\"\n";
};
/* End PBXBuildRule section */

Expand Down Expand Up @@ -346,7 +346,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "bash $SRCROOT/xc-universal-binary.sh libarithmetical.a uniffi-example-arithmetic $SRCROOT/../../.. $CONFIGURATION\n";
shellScript = "bash $SRCROOT/xc-universal-binary.sh uniffi-example-arithmetic $SRCROOT/../../.. $CONFIGURATION\n";
};
CEEB59EB25263ECE003C87D1 /* Build Universal Binary for todolist */ = {
isa = PBXShellScriptBuildPhase;
Expand All @@ -364,7 +364,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "bash $SRCROOT/xc-universal-binary.sh libuniffi_todolist.a uniffi-example-todolist $SRCROOT/../../.. $CONFIGURATION\n";
shellScript = "bash $SRCROOT/xc-universal-binary.sh uniffi-example-todolist $SRCROOT/../../.. $CONFIGURATION\n";
};
/* End PBXShellScriptBuildPhase section */

Expand Down Expand Up @@ -543,7 +543,9 @@
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SRCROOT)/../../../target/universal/debug";
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "../../../target/aarch64-apple-ios/debug";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "../../../target/aarch64-apple-ios-sim/debug";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "../../../target/x86_64-apple-ios/debug";
PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.example.uniffi.IOSApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "IOSApp-Bridging-Header.h";
Expand All @@ -567,7 +569,9 @@
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SRCROOT)/../../../target/universal/release";
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "../../../target/aarch64-apple-ios/release";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "../../../target/aarch64-apple-ios-sim/release";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "../../../target/x86_64-apple-ios/release";
PRODUCT_BUNDLE_IDENTIFIER = org.mozilla.example.uniffi.IOSApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "IOSApp-Bridging-Header.h";
Expand Down
65 changes: 35 additions & 30 deletions examples/app/ios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ This will not be a complete tutorial on how to use `uniffi`, just the bits to ge
## Install Rust compiler targets with `rustup`

```sh
% rustup target add x86_64-apple-ios aarch64-apple-ios
rustup target add x86_64-apple-ios aarch64-apple-ios aarch64-apple-ios-sim
```

## Creating the bindgen binary

See [Creating the bindgen binary](https://mozilla.github.io/uniffi-rs/tutorial/foreign_language_bindings.html#foreign-language-bindings)
for a full guide on how to create a uniffi-bindgen library for your project.

This sample project ships with one in `examples/app/uniffi-bindgen-cli`.

## Associate the iOS project with the Rust project

1. In Xcode, right click on the project in the Project Navigator.
Expand All @@ -23,17 +30,17 @@ This will not be a complete tutorial on how to use `uniffi`, just the bits to ge
1. In Xcode, click on the project in the Project Navigator.
2. In the main window, select the app's main target, and then select "Build Rules".
3. Add a custom rule, to process sources files with names matching `*.udl`.
4. Use `uniffi-bindgen` on each file to generate the headers and swift scaffolding.
4. Use your `uniffi-bindgen` binary on each file to generate the headers and swift scaffolding.

```sh
$HOME/.cargo/bin/uniffi-bindgen generate "$INPUT_FILE_PATH" --language swift --out-dir "$DERIVED_FILE_DIR"
$HOME/.cargo/bin/cargo run -p uniffi-bindgen -- generate "$INPUT_FILE_PATH" --language swift --out-dir "$DERIVED_FILE_DIR"
```

These will output two files Xcode is interested in.

```sh
$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE)FFI.h
$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE).swift
$(DERIVED_FILE_DIR)/uniffi_$(INPUT_FILE_BASE)-Bridging-Header.h
```

The header file is a descriptor of the C API to a library that hasn't been built yet. The Swift file is a Swift facade that calls that C API.
Expand All @@ -49,91 +56,89 @@ The header file is a descriptor of the C API to a library that hasn't been built
#ifndef IOSApp_Bridging_Header_h
#define IOSApp_Bridging_Header_h

#import "todolist-Bridging-Header.h"
#import "todolistFFI.h"

#endif /* IOSApp_Bridging_Header_h */
```

The `#import` directive points to the header file generated by `uniffi-bindgen generate` above— `$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE)-Bridging-Header.h`
The `#import` directive points to the header file generated by `uniffi-bindgen generate` above— `$(DERIVED_FILE_DIR)/$(INPUT_FILE_BASE)FFI.h`

```h
#import "todolist-Bridging-Header.h"
#import "todolistFFI.h"
```

This `IOSApp-Bridging-Header.h` tells Xcode to look for the header file; because it's in `DERIVED_FILE_DIR`, Xcode should know where to look.

## Configure `cargo` to build the crate as a static library

1. In the `Cargo.toml` file of the Rust project, add `staticlib` to the `crate-type` list.
1. The build script automatically builds a static library (note: this requires rustc 1.64 or newer)

```toml
[lib]
crate-type = ["staticlib", "cdylib"]
name = "uniffi_todolist"
```

The `package` `name` and the `lib` `name` will be used below. In this case, the package name is `uniffi-example-todolist` and the lib name is `uniffi_todolist`.

## Tell Xcode how to build the Rust project.

#### TODO use a better explanation of what is going on here

1. In Xcode, click on the project in the Project Navigator.
2. In the main window, select the app's main target, and then select "Build Phases".
3. Add a new `Run Script` build phase and move it to the top.
4. Add the script that will build a universal binary for the Rust project.

For this project, we've used a script adapted from the #mozilla/application-services project to build a universal binary with `lipo`.
`uniffi` comes with a script suitable to build a static library for all iOS targets:

```sh
bash $SRCROOT/xc-universal-binary.sh libuniffi_todolist.a uniffi-example-todolist $SRCROOT/../../../ $CONFIGURATION
bash $SRCROOT/xc-universal-binary.sh uniffi-example-todolist $SRCROOT/../../../ $CONFIGURATION
```

In this case we constructed the command:

```sh
xc-universal-binary.sh <STATIC_LIB_NAME> <FFI_TARGET> <WORKSPACE_PATH> <BUILD_CONFIGURATION>"
xc-universal-binary.sh <FFI_TARGET> <WORKSPACE_PATH> <BUILD_CONFIGURATION>"
```

by making:

* `STATIC_LIB_NAME` from the `lib` `name` above: `uniffi_todolist` --> `libuniffi_todolist.a`
* `FFI_TARGET` from the `package` `name` above: `uniffi-example-todolist`.

The workspace path is where the `Cargo.toml` will resolve the Rust project, and also determine the target directory that `cargo build` and `lipo` will put its artifacts.

This script performs a few steps:

1. Runs `cargo build` to compile the Rust project for the `x86_64-apple-ios` and `aarch64-apple-ios` targets.
* This includes using `uniffi-bindgen` to generate the Rust scaffolding so we can go from C to Rust.
2. Runs `lipo` to combine these libs in to a universal binary.
3. Puts the universal binary in to the `$WORKSPACE_PATH/target/universal` directory.
This script runs `cargo build` to compile the Rust project for the `x86_64-apple-ios`, `aarch64-apple-ios` and `aarch64-apple-ios-sim` targets.
This includes using `uniffi-bindgen` to generate the Rust scaffolding so we can go from C to Rust.

## Tell Xcode where the universal library is

Finally, we need to tell Xcode to look for the universal binary `libuniffi_todolist.a` is, so it can tie it together with the header file `todolist-Bridging-Header.h`.
Finally, we need to tell Xcode to look for the libraries based on the target, so it can tie it together with the header file `todolistFFI.h`.

1. In Xcode, click on the project in the Project Navigator.
2. In the main window, select the app's main target, and then select "Build Settings".
3. Search for `Library Search Paths`.
4. Add paths to where lipo constructed the universal binaries for each of `Debug` and `Release`.
4. Add paths per OS and architecture.
The OS specific part can be configured in the Xcode project editor,
but the architecture specific part has to be added by manually editing the `project.pbxproj` file.
Open `project.pbxproj` in your project direcotry and add the following in the `Debug` section:

```sh
$(SRCROOT)/../../../target/universal/debug
$(SRCROOT)/../../../target/universal/release
```
```
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = "../../../target/aarch64-apple-ios/debug";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = "../../../target/aarch64-apple-ios-sim/debug";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = "../../../target/x86_64-apple-ios/debug";
```

The path should be a relative path pointing into the `target` directory created by `cargo`.
Add a similar block in the `Release` section and adjust the paths accordingly.

## Back to code.

By now, we should've
By now, we should've

1. Used `uniffi-bindgen` to generate a Swift file to call a C API header and put them in a place Xcode can find them.
2. Configured Xcode to find the header.
3. Configured cargo to build a static library version of the crate.
4. Used `uniffi-bindgen` to generate a C API for the Rust crate.
5. Used cargo to cross-compile the crate for different targets.
6. Used lipo to combine those crates for different target into a universal binary.
7. Configured Xcode to find the universal binary.
7. Configured Xcode to find the different static libraries.

By now, we should be able to hit Run in Xcode and build something.

Expand Down
77 changes: 40 additions & 37 deletions examples/app/ios/xc-universal-binary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,57 @@ trap error_help ERR
PATH="$(bash -l -c 'echo $PATH')"

# This should be invoked from inside xcode, not manually
if [[ "${#}" -ne 4 ]]
if [[ "${#}" -ne 3 ]]
then
echo "Usage (note: only call inside xcode!):"
echo "path/to/build-scripts/xc-universal-binary.sh <STATIC_LIB_NAME> <FFI_TARGET> <SRC_ROOT_PATH> <buildvariant>"
echo "path/to/build-scripts/xc-universal-binary.sh <FFI_TARGET> <SRC_ROOT_PATH> <buildvariant>"
exit 1
fi
# e.g. liblogins_ffi.a
STATIC_LIB_NAME=${1}
# what to pass to cargo build -p, e.g. logins_ffi
FFI_TARGET=${2}
# path to app services root
SRC_ROOT=${3}
# buildvariant from our xcconfigs
BUILDVARIANT=$(echo "${4}" | tr '[:upper:]' '[:lower:]')
FFI_TARGET=${1}
# path to source code root
SRC_ROOT=${2}
# buildvariant from our xcconfigs
BUILDVARIANT=$(echo "${3}" | tr '[:upper:]' '[:lower:]')

RELFLAG=
RELDIR="debug"
if [[ "${BUILDVARIANT}" != "debug" ]]; then
RELFLAG=--release
RELDIR=release
fi

TARGETDIR=${SRC_ROOT}/target
if [[ -n "${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="${SDK_DIR}/usr/lib:${LIBRARY_PATH:-}"
fi

# We can't use cargo lipo because we can't link to universal libraries :(
# https://github.com/rust-lang/rust/issues/55235
LIBS_ARCHS=("x86_64" "arm64")
IOS_TRIPLES=("x86_64-apple-ios" "aarch64-apple-ios")
for i in "${!LIBS_ARCHS[@]}"; do
env -i PATH="${PATH}" \
"${HOME}"/.cargo/bin/cargo build --locked -p "${FFI_TARGET}" --lib ${RELFLAG} --target "${IOS_TRIPLES[${i}]}"
done
IS_SIMULATOR=0
if [ "${LLVM_TARGET_TRIPLE_SUFFIX-}" = "-simulator" ]; then
IS_SIMULATOR=1
fi

UNIVERSAL_BINARY=${TARGETDIR}/universal/${RELDIR}/${STATIC_LIB_NAME}
NEED_LIPO=
for arch in $ARCHS; do
case "$arch" in
x86_64)
if [ $IS_SIMULATOR -eq 0 ]; then
echo "Building for x86_64, but not a simulator build. What's going on?" >&2
exit 2
fi

# if the universal binary doesnt exist, or if it's older than the static libs,
# we need to run `lipo` again.
if [[ ! -f "${UNIVERSAL_BINARY}" ]]; then
NEED_LIPO=1
elif [[ "$(stat -f "%m" "${TARGETDIR}/x86_64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}")" -gt "$(stat -f "%m" "${UNIVERSAL_BINARY}")" ]]; then
NEED_LIPO=1
elif [[ "$(stat -f "%m" "${TARGETDIR}/aarch64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}")" -gt "$(stat -f "%m" "${UNIVERSAL_BINARY}")" ]]; then
NEED_LIPO=1
fi
if [[ "${NEED_LIPO}" = "1" ]]; then
mkdir -p "${TARGETDIR}/universal/${RELDIR}"
lipo -create -output "${UNIVERSAL_BINARY}" \
"${TARGETDIR}/x86_64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}" \
"${TARGETDIR}/aarch64-apple-ios/${RELDIR}/${STATIC_LIB_NAME}"
fi
# Intel iOS simulator
export CFLAGS_x86_64_apple_ios="-target x86_64-apple-ios"
$HOME/.cargo/bin/cargo rustc -p "${FFI_TARGET}" --lib --crate-type staticlib $RELFLAG --target x86_64-apple-ios
;;

arm64)
if [ $IS_SIMULATOR -eq 0 ]; then
# Hardware iOS targets
$HOME/.cargo/bin/cargo rustc -p "${FFI_TARGET}" --lib --crate-type staticlib $RELFLAG --target aarch64-apple-ios
else
# M1 iOS simulator -- currently in Nightly only and requires to build `libstd`
$HOME/.cargo/bin/cargo rustc -p "${FFI_TARGET}" --lib --crate-type staticlib $RELFLAG --target aarch64-apple-ios-sim
fi
esac
done
12 changes: 12 additions & 0 deletions examples/app/uniffi-bindgen-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "uniffi-bindgen-cli"
version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"

[dependencies]
uniffi = { path = "../../../uniffi", features = ["cli"] }
3 changes: 3 additions & 0 deletions examples/app/uniffi-bindgen-cli/uniffi-bindgen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_main()
}