Skip to content

swift-java cannot bridge members generated by macros #547

@omochi

Description

@omochi

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)
}
  1. For ImplicitInternalInitCat, the compiler generates an internal init by default. This is not bridged to Java, which is the expected and natural behavior since only public members should be bridged.
  2. For ExplicitPublicInitCat, it has a manually written public init, which is correctly bridged to Java. This is OK.
  3. The problem lies with MacroGenPublicInitCat. This struct uses the @MemberwiseInit macro to generate a public init. 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:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions