-
Notifications
You must be signed in to change notification settings - Fork 78
Description
Problem Statement
swift-java reads Swift source files to generate Java code for bindings. Since these source files are in a state prior to macro expansion, the tool cannot recognize members generated by macros. Consequently, these members cannot be bound or utilized from Java.
To be more precise, this might be a limitation of the current build configuration in swift-android-examples rather than an inherent issue with swift-java itself.
Please let me know if my categorization of this issue is inappropriate.
Actual Example
Suppose we want to bridge the following three structs using swift-java:
import MemberwiseInit
public struct ImplicitInternalInitCat {
public var name: String
}
public struct ExplicitPublicInitCat {
public init(name: String) {
self.name = name
}
public var name: String
}
@MemberwiseInit(.public)
public struct MacroGenPublicInitCat {
public var name: String
}This generates the following Java bridges (irrelevant parts omitted):
public final class ImplicitInternalInitCat implements JNISwiftInstance {
private ImplicitInternalInitCat(long selfPointer, SwiftArena swiftArena)
}
public final class ExplicitPublicInitCat implements JNISwiftInstance {
private ExplicitPublicInitCat(long selfPointer, SwiftArena swiftArena)
public static ExplicitPublicInitCat init(java.lang.String name, SwiftArena swiftArena$)
}
public final class MacroGenPublicInitCat implements JNISwiftInstance {
private MacroGenPublicInitCat(long selfPointer, SwiftArena swiftArena)
}- For
ImplicitInternalInitCat, the compiler generates aninternalinit by default. This is not bridged to Java, which is the expected and natural behavior since onlypublicmembers should be bridged. - For
ExplicitPublicInitCat, it has a manually writtenpublicinit, which is correctly bridged to Java. This is OK. - The problem lies with
MacroGenPublicInitCat. This struct uses the@MemberwiseInitmacro to generate apublicinit. However, it is not bridged to Java because the members generated by the macro are not being targeted.
For details on the macro's behavior, please refer to the distribution source:
Essentially, it reimplements the compiler's auto-generated init but allows it to be made public via parameters, which is very convenient.
I have published the source code where I verified this build in my environment below. The build configuration is almost identical to that of swift-android-examples.
Background
In my project, I share source code between an API server implemented in Swift and an iOS app. This allows us to rely on Codable for API serialization, virtually eliminating transport layer issues.
As a result, many objects and response structures used in the API are implemented as Swift structs. Because the shared parts are separated into modules for build purposes, members generally need to be implemented as public.
Implementing initializers manually for so many structs leads to significant boilerplate and effort, so I use macros to improve development efficiency.
I am now attempting to use this API definition module directly in an Android app using swift-java. However, because macro-defined members are unavailable, I cannot construct API request types from Java, which is quite inconvenient.
Consideration of Solutions
I believe there are several possible directions to resolve this. I would like to share my ideas for reference.
1. Have swift-java utilize .swiftinterface files
Swift source code contains symbols as they are written before macro expansion. On the other hand, .swiftinterface files are header files where macros are already expanded. Since they are written in Swift syntax, swift-java could potentially read these to bridge macro-generated members.
However, this approach seems to have several challenges:
No SwiftPM functionality to generate .swiftinterface
While .swiftinterface can be output using the compiler CLI, SwiftPM does not provide an interface for this. Actual project source code cannot be compiled without setting up library and target dependencies, making it difficult to pass files to the compiler without using a SwiftPM package manifest.
It might be necessary to add this functionality to the SwiftPM CLI or API.
Requirement for Library Evolution mode
My understanding is that to output a .swiftinterface, the target must be compiled in "Library Evolution" mode. This mode is not "free." It changes the semantics of the source code (e.g., how enums are handled), so it cannot always be enabled easily. It would require adjustments to the target and the consuming code.
When I tried this previously for another matter, I found that swift-atomics could not be built with Library Evolution enabled.
Similarly, SwiftPM lacks a way to set this mode on a per-target basis; providing it as a CLI option via -Xswiftc affects all dependent libraries as well.
We might need a feature in SwiftPM or the toolchain to output .swiftinterface for targets in standard mode, rather than in library evolution mode.
2. Expand macros beforehand
Even without generating a full .swiftinterface, the result of macro expansion is still Swift source code. If we could obtain source code with only the macros expanded, swift-java could read that to perform the bridging.
Xcode has a right-click menu option to expand macros (though it often bugs out), so the necessary underlying mechanism might already exist.
For example, if the SwiftPM CLI had a function to target a directory of source files and generate a new source tree in another directory with all macros expanded, we could utilize that in a Gradle script. This would allow swift-java to resolve the issue without any internal changes.
My post on the Swift forums was written with this idea in mind: