y-crdt/yswift

Cross-compilation for all platforms

nugmanoff opened this issue Β· 9 comments

Currently only iOS is supported. Goal is to add support for macOS, tvOS & watchOS as well.

Maybe as part of this we can work on simplification of iOS infra setup. Current setup is quite clumsy, and I think it can be improved. Few ideas:

  • Create xcframework with xcodebuild --create-xcframework. Right now it is created by hand by laying out proper directory structure with some supporting files (.h, .modulemap).
  • Remove redundant wrapping of uniffi-generated .h file. Due to historical reasons😁, header file that is generated by uniffi as part of the scaffolding process is then imported by umbrella header, which I believe is not necessary.
  • Remove @_exported import with ynative-exported.swift, this one is related to the previous point.
heckj commented

^^ All sounds good, and happy to advance that script. I don't think there's a notably better process than scripting at the moment, at least in terms of generating the swift-oriented outputs.

Due to the nature of Rust and its platform options, watchOS, tvOS, and MacCatalyst related platforms are currently only supported on the "nightly" edition of Rust. Because of that, I'd suggest we limit the platform support to iOS, macOS, and perhaps swift on Linux as a stretch goal. Linux is more a personal whim, because I'd love to see this library as something available to server-side swift (Vapor, Hummingbird, etc) - where I think it could also be extremely useful.

heckj commented

I've gone back to work on this this a second time, and I'm hitting a very inscrutable error reproducing a functional XCFramework with static libraries using the xcodebuild -xcframework command. When I re-create it, I'm running into the compiler reporting there's no module to import. After doing quite a bit a research yesterday, I found 1) that I'm not the only one hitting that issue and 2) that there's a comment in the Xcode release notes about this very scenario.

From the release notes with Xcode 14:

Using Swift packages with binary targets may result in a β€œno such module” error when attempting to import the module of a binary target. (77465707)

The number is the internal Apple bug number, and I've no idea if it's still in play or if there are workarounds available. There are some hints that adding an Objective-C bridging header to the C library might mitigate some of these issues. In any case, the structure that @nugmanoff set up continues to work - so instead of switching the process around, I may just double-down on the setup that's there and expand it so that it has listings for macOS x86 & arm processors as well.

At least now I know that I wasn't the only one to wrestle with this. Feels soothing 😁

Because usually "Can't find module" errors are really low-hanging fruits (like dyld search paths & etc), but looks like this is not the case.

Thanks to Mozilla guys whose script I'd used as a reference - we at least have something functional.

heckj commented

word from a friend is that this issue was opened, and immediately resolved, only applying the Xcode 13.2 beta 2 - so I suspect there's something else at play there. I think I might open a DTS incident to get some assistance with my process of making the XCFramework. I know (and have found some) of the constraints - like it's name needs to be identical to the module being exported - but I suspect there's others in there, and I might be running afoul of something.

heckj commented

This failure was sufficiently bothersome and opaque that I decided to open (spend) a Technical Support Incident from my developer account. Thought I'd document what I constructed here as a backup copy while I get their assistance.

Code Level Support from Apple Developer Tech Support
(aka TSI - technical support incident)

https://developer.apple.com/contact/technical/#!/request/form

Title

Failure to import a module after constructing a multi-platform XCFramework from static binaries

Platform (iOS and iPadOS)

macOS X Ventura (13.2)
Xcode Version 14.2 (14C18)
swift -version:

swift-driver version: 1.62.15 Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
Target: arm64-apple-macosx13.0

Full Description

The Y-CRDT project (http://github.com/y-crdt/) has written core engine binaries in Rust, and is
generating a swift language binding for their library. The language bindings are using an
FFI interface through swift, with the libraries themselves exported as static libraries compiled
for the relevant platforms.

In the project http://github.com/heckj/y-uniffi, where I'm working on this setup, I'm creating
an XCFramework using xcodebuild -create-xcframework. However, the resulting framework doesn't
appear to be recognized by swift when I'm using that XCFramework as a local reference. The associated
swift code imports the framework as import yniffiFFI, which in Xcode results in the error:
No such module 'yniffiFFI'.

The project is using Uniffi (from Mozilla), and is currently hand-assembling an XCFramework
directory structure - which is working and importing correctly. Replacing that process with
an XCFramework that's created using xcodebuild -create-xcframework is when we run into this
error.

The build script to create the framework is avaiable in the project, in the lib directory:
build-xcframework.sh. As a general description it sets up Rust and cargo commands, builds the
libraries for the various platforms. Uses lipo to combine single-platform static libraries (.a files)
into multi-platform versions, and finally passes those fat versions, along with the headers and modulemap,
into the xcodebuild command.

The resulting XCFramework can then be imported in a new sample project, but the import yniffiFFI doesn't find
the C library, and I don't have much diagnostic detail on how to determine what I might have missed.

The structure of the XCFramework that's built is simpler than the hand-generated one from the Mozilla project
as well, making me wonder if I'm missing something.

The structure of the new version:

yniffiFFI.xcframework
β”œβ”€β”€ Info.plist
β”œβ”€β”€ ios-arm64
β”‚   β”œβ”€β”€ Headers
β”‚   β”‚   β”œβ”€β”€ yniffiFFI.h
β”‚   β”‚   └── yniffiFFI.modulemap
β”‚   └── libuniffi_yniffi.a
└── ios-arm64_x86_64-simulator
    β”œβ”€β”€ Headers
    β”‚   β”œβ”€β”€ yniffiFFI.h
    β”‚   └── yniffiFFI.modulemap
    └── libuniffi_yniffi.a

The structure of the original (also named differently):

YniffiXC.xcframework
β”œβ”€β”€ Info.plist
β”œβ”€β”€ ios-arm64
β”‚   └── YniffiXC.framework
β”‚       β”œβ”€β”€ Headers
β”‚       β”‚   β”œβ”€β”€ YniffiXC.h
β”‚       β”‚   └── yniffiFFI.h
β”‚       β”œβ”€β”€ Modules
β”‚       β”‚   └── module.modulemap
β”‚       └── YniffiXC
└── ios-arm64_x86_64-simulator
    └── YniffiXC.framework
        β”œβ”€β”€ Headers
        β”‚   β”œβ”€β”€ YniffiXC.h
        β”‚   └── yniffiFFI.h
        β”œβ”€β”€ Modules
        β”‚   └── module.modulemap
        └── YniffiXC

With the largest difference being naming of files and having a separate MODULES directory that contains
an umbrella framework modulemap.

Within the lib directory is a Package.swift file that includes associated Swift code to expand on
the raw C level interfaces.

The core questions are:

  • Why isn't the yniffiFFI module being exposed to the swift code?
  • Are there constraints in library names, file names, or means of providing the modulemap to the header that are related?
  • What's the best way to create this XCFramework?

The code in question is fully public and available:

In the DTS branch, I've checked in all the binaries I generated in case that makes
investigating this easier. By running ./build-xcframework.sh again, it will
overwrite the files, but should be an otherwise consistent rebuild process.

I did see an earlier reference in an Xcode release notes to an issue which appeared similar:

Using Swift packages with binary targets may result in a β€œno such module” error when attempting to import the module of a binary target. (77465707)

But I've also been told this issue was thought resolved in Xcode 13 beta 2, and I've no insight to
the original report to know if it's related or not, represents a regression, or something else at play.

Steps to reproduce

To completely reproduce my build steps, you'll need to have Rust installed locally on your development machine:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

git clone http://github.com/heckj/y-uniffi
cd y-uniffi
git checkout DTS
cd lib
./build-xcframework.sh

# This generates the XCFramework `yniffiFFI.xcframework` which fails on import.

# => open Package.swift with Xcode
# => switch target to "Any iOS Device"
# => note build errors resulting from failed import
heckj commented

I won't put all the back-and-forth in here, but this bit's potentially useful:

Thank you for contacting Apple Developer Technical Support. This acknowledgment is automatically generated and does not require a reply.

Follow-up: 818959140

Your request has been assigned the follow-up number listed at the top of this message. When submitting a follow-up email for this request, please include the follow-up number on the first line of your message. Here is the format you should use: Follow-up: 818959140

A Technical Support Incident (TSI) will be debited from your developer account for this inquiry. Additional TSIs are available for purchase in the Code-Level Support section of your account.
heckj commented

I also started a swift forum thread on techniques to debug (https://forums.swift.org/t/debugging-an-xcframework-module-not-available-for-import/62986) - got a few hints that will be useful, but only relevant when building and importing for macOS (so it doesn't solve the broader quandary)

Still - nice to learn the debugging potentials there:

  • swift build -verbose
  • debugging argument: -Rmodule-loading