Skip to content
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
31 changes: 29 additions & 2 deletions Sources/CodePrinting/CodePrinter+Java.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,21 @@ extension CodePrinter where Language == JavaLanguage {
///
/// Shape depends on `options.sourceVersion`:
/// - Java 23+: Markdown-style line comments (`///`) based on https://openjdk.org/jeps/467
/// - Earlier versions: classic `/** ... */` block, with each body line
/// `{@code X}` inline tags are rewritten to Markdown backticks (`` `X` ``).
/// - Earlier versions: classic `/** ... */` block. Single-line input is
/// collapsed to `/** text */`; multi-line input keeps each body line
/// prefixed by ` * ` (just ` *` for blank paragraph separators).
///
/// Pass paragraph breaks as blank lines (`\n\n`) inside `text`.
public mutating func printJavadocComment(_ text: String) {
let lines = text.split(separator: "\n", omittingEmptySubsequences: false)
if options.sourceVersion >= 23 {
for line in lines {
print(line.isEmpty ? "///" : "/// \(line)")
let rewritten = Self.rewriteInlineCodeTagsToMarkdown(String(line))
print(rewritten.isEmpty ? "///" : "/// \(rewritten)")
}
} else if lines.count == 1 {
print("/** \(lines[0]) */")
} else {
print("/**")
for line in lines {
Expand All @@ -50,4 +55,26 @@ extension CodePrinter where Language == JavaLanguage {
print(" */")
}
}

/// Rewrite `{@code X}` inline Javadoc tags to Markdown backtick spans
/// (`` `X` ``) for JEP 467 `///` comments.
static func rewriteInlineCodeTagsToMarkdown(_ line: String) -> String {
var result = ""
var remaining = line[...]
while let start = remaining.range(of: "{@code ") {
result.append(contentsOf: remaining[..<start.lowerBound])
let afterOpen = start.upperBound
guard let closeIdx = remaining[afterOpen...].firstIndex(of: "}") else {
// No closing brace, bail out and keep the tail as-is
result.append(contentsOf: remaining[start.lowerBound...])
return result
}
result.append("`")
result.append(contentsOf: remaining[afterOpen..<closeIdx])
result.append("`")
remaining = remaining[remaining.index(after: closeIdx)...]
}
result.append(contentsOf: remaining)
return result
}
}
48 changes: 48 additions & 0 deletions Sources/JExtractSwiftLib/Convenience/String+JavaNaming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

import SwiftExtract

extension String {
/// Returns whether the string is of the format `isX` (Java Beans boolean
/// property naming convention)
Expand All @@ -23,4 +25,50 @@ extension String {
let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2)
return self[thirdCharacterIndex].isUppercase
}

/// Joins components into a flat name, example: `JavaOuter_Inner_run_cb`
package static func flatName(
prefix: String? = nil,
parent: SwiftQualifiedTypeName,
method: String,
parameter: String,
) -> String {
"\(prefix ?? "")\(parent.fullFlatName)_\(method)_\(parameter)"
}

/// Joins components with `.`, example: `Outer.Inner.run.cb`
package static func javaQualifiedName(_ components: String...) -> String {
components.joined(separator: ".")
}

/// Java binary name for a nested synthetic type, example: `com.example.Outer$Inner$run$cb`
///
/// - SeeAlso: [JLS 13.1 - The Form of a Binary](https://docs.oracle.com/javase/specs/jls/se21/html/jls-13.html#jls-13.1)
package static func javaBinaryName(
package javaPackage: String,
parent: SwiftQualifiedTypeName,
method: String,
qualifier: String? = nil,
) -> String {
let packagePrefix = javaPackage.isEmpty ? "" : "\(javaPackage)."
let qualifierSuffix = qualifier.map { "$\($0)" } ?? ""
return "\(packagePrefix)\(parent.jniEscapedName)$\(method)\(qualifierSuffix)"
}

/// JNI C symbol name for a `@_cdecl` native method entry point, example:
/// `Java_com_example_Outer_00024Inner_run__I` (`Java_<package>_<Class>_<method>__<sig>`).
/// - SeeAlso: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names
package static func jniSymbolName(
package javaPackage: String,
parent: SwiftQualifiedTypeName,
method: String,
signature: String,
) -> String {
"Java_"
+ javaPackage.replacingOccurrences(of: ".", with: "_")
+ "_\(parent.jniEscapedName.escapedJNIIdentifier)_"
+ method.escapedJNIIdentifier
+ "__"
+ signature.escapedJNIIdentifier
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,6 @@ extension FFMSwift2JavaGenerator {
}

/// Print the helper type container for a user-facing Java API.
///
/// * User-facing functional interfaces.
func printJavaBindingWrapperHelperClass(
_ printer: inout JavaPrinter,
_ decl: ExtractedFunc,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import CodePrinting
import SwiftExtract
import SwiftJavaJNICore

extension JNISwift2JavaGenerator {

/// Emit `@JavaInterface` Swift wrappers for each `@escaping` closure
/// parameter of the passed in function.
///
/// The wrapper's purpose is to retain the Java side object and delegate the calls on it to
/// the Java object "callback".
/// Each wrapper has it's corresponding Java side printed by `printJavaBindingWrapperHelperClass`.
func printEscapingClosureSwiftFunctionHelperClasses(
_ printer: inout SwiftPrinter,
_ translatedDecl: TranslatedFunctionDecl,
) {
for ty in translatedDecl.functionTypes {
guard let closureTy = ty.syntheticClosureType else {
continue
}
printEscapingClosureWrapJavaInterface(&printer, closureTy)
}
}

private func printEscapingClosureWrapJavaInterface(
_ printer: inout SwiftPrinter,
_ closureTy: SyntheticEscapingClosureFunctionType,
) {
printer.printBraceBlock(
"""
@JavaInterface("\(closureTy.javaBinaryName)")
public struct \(closureTy.javaInterfaceName)
"""
) { p in
let signature = self.renderEscapingClosureApplySignature(closureTy.functionType)
p.print(
"""
@JavaMethod
public func apply\(signature)
"""
)
}
printer.println()
}

private func renderEscapingClosureApplySignature(_ type: SwiftFunctionType) -> String {
let params = type.parameters.enumerated().map { idx, param -> String in
let name = param.parameterName ?? "_\(idx)"
let typeName = param.type.description
return "_ \(name): \(typeName)"
}
let paramList = "(\(params.joined(separator: .comma)))"

if type.resultType.isVoid {
return paramList
} else {
return "\(paramList) -> \(type.resultType.description)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,13 @@ extension JNISwift2JavaGenerator {
}

/// Represents a synthetic protocol-like translation for escaping closures.
/// This allows closures to use the same conversion infrastructure as protocols,
/// providing support for optionals, arrays, custom types, async, etc.
struct SyntheticClosureFunction {
/// The wrap-java interface name (e.g., "JavaMyClass.setCallback.callback")
let wrapJavaInterfaceName: String
struct SyntheticEscapingClosureFunctionType {
/// The name of the Java functional interface to be generated.
let javaInterfaceName: String

/// Java binary name of the corresponding `@FunctionalInterface`.
/// E.g. `com.example.swift.CallbackManager$setCallback$callback`.
let javaBinaryName: String

/// Conversion steps for each parameter
let parameterConversions: [UpcallConversionStep]
Expand Down Expand Up @@ -142,8 +144,9 @@ extension JNISwift2JavaGenerator {
/// allowing it to use the same conversion infrastructure for optionals, arrays, etc.
func generateSyntheticClosureFunction(
functionType: SwiftFunctionType,
wrapJavaInterfaceName: String
) throws -> SyntheticClosureFunction {
javaInterfaceName: String,
javaBinaryName: String,
) throws -> SyntheticEscapingClosureFunctionType {
let parameterConversions = try functionType.parameters.enumerated().map { idx, param in
try self.translateParameter(
parameterName: param.parameterName ?? "_\(idx)",
Expand All @@ -156,8 +159,9 @@ extension JNISwift2JavaGenerator {
methodName: "apply"
)

return SyntheticClosureFunction(
wrapJavaInterfaceName: wrapJavaInterfaceName,
return SyntheticEscapingClosureFunctionType(
javaInterfaceName: javaInterfaceName,
javaBinaryName: javaBinaryName,
parameterConversions: parameterConversions,
resultConversion: resultConversion,
functionType: functionType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,6 @@ extension JNISwift2JavaGenerator {
}

/// Print the helper type container for a user-facing Java API.
///
/// * User-facing functional interfaces.
private func printJavaBindingWrapperHelperClass(
_ printer: inout JavaPrinter,
_ decl: ExtractedFunc,
Expand Down Expand Up @@ -675,6 +673,11 @@ extension JNISwift2JavaGenerator {
) {
let apiParams = functionType.parameters.map({ $0.parameter.renderParameter() })

printer.printJavadocComment(
"""
Corresponds to the Swift closure parameter of type {@code \(functionType.swiftType)}.
"""
)
printer.print(
"""
@FunctionalInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,24 @@ extension JNISwift2JavaGenerator {

switch param.type {
case .function(let funcTy):
let translatedClosure = try translateFunctionType(
var translatedClosure = try translateFunctionType(
name: parameterName,
swiftType: funcTy,
parentName: parentName,
)
// Lift the escaping-closure spec off the matching `NativeParameter`
// onto the `TranslatedFunctionType`. This keeps the wrap-java
// emission colocated with the decl it belongs to, so the thunk
// printer can emit the `@JavaInterface` Swift struct alongside
// the cdecl thunk that references it - no generator-level
// sink/dict/pre-pass required. `NativeParameter` runs 1:1 with
// the Swift parameters here (a Swift param can lower to multiple
// *JavaParameter*s but stays one `NativeParameter`), so indexing
// by `idx` is correct.
if idx < nativeFunctionSignature.parameters.count {
translatedClosure.syntheticClosureType =
nativeFunctionSignature.parameters[idx].syntheticClosure
}
Comment thread
ktoso marked this conversation as resolved.
funcTypes.append(translatedClosure)
default:
break
Expand Down Expand Up @@ -508,7 +521,10 @@ extension JNISwift2JavaGenerator {
return TranslatedParameter(
parameter: JavaParameter(
name: parameterName,
type: .class(package: javaPackage, name: "\(parentName.fullName).\(methodName).\(parameterName)"),
type: .class(
package: javaPackage,
name: String.javaQualifiedName(parentName.fullName, methodName, parameterName)
),
annotations: parameterAnnotations,
),
conversion: .placeholder,
Expand Down Expand Up @@ -1705,6 +1721,17 @@ extension JNISwift2JavaGenerator {
var parameters: [TranslatedParameter]
var result: TranslatedResult
var swiftType: SwiftFunctionType

var isEscaping: Bool { swiftType.isEscaping }

/// Represents this `TranslatedFunctionType` if we need to create a synthetic protocol
/// to handle the cross-language call to the function (closure).
///
/// E.g. if the function type we're in is `@escaping` we need to create an
/// interface on the Java side to represent it, and a Swift side wrapper to
/// retain/wrap it, that will be then called into from the escaping Swift-side
/// closure we forward to the downcall target.
var syntheticClosureType: SyntheticEscapingClosureFunctionType?
}

/// Describes how to convert values between Java types and the native Java function
Expand Down
Loading
Loading