Skip to content

Commit 93d7c37

Browse files
committed
jextract/jni: Specialization and constrained extension support
This allows specializing types e.g. Box<T> with a typealias `FishBox = Box<Fish>` becomes its own type in Java and gets all methods from Box but also any methods where the `extension Box where Element == Fish {}`. This gives Java access to previously unavailable APIs in a safe way. Though yes it makes the type different in Java - so one may have to convert to a Box<> sometimes etc. We may need more helper methods for those. For now I want to enable getting to those otherwise unavailable methods via specialization. Specialization is available via a typealias trigger, or via configuration which is nice if you don't control the sources you're trying to specialize.
1 parent 9c44625 commit 93d7c37

20 files changed

Lines changed: 1293 additions & 324 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
public struct Box<Element> {
16+
public var count: Int64
17+
18+
public init(count: Int64) {
19+
self.count = count
20+
}
21+
}
22+
23+
public struct Fish {
24+
public var name: String
25+
26+
public init(name: String) {
27+
self.name = name
28+
}
29+
}
30+
31+
extension Box where Element == Fish {
32+
public func describeFish() -> String {
33+
"A box of \(count) fish"
34+
}
35+
}
36+
37+
public typealias FishBox = Box<Fish>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024-2026 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
package com.example.swift;
16+
17+
import org.junit.jupiter.api.Test;
18+
19+
import java.lang.reflect.Method;
20+
21+
import static org.junit.jupiter.api.Assertions.*;
22+
23+
public class BoxSpecializationTest {
24+
@Test
25+
void fishBoxHasExpectedMethods() throws Exception {
26+
// Verify FishBox class exists and has the expected methods
27+
Class<?> fishBoxClass = FishBox.class;
28+
assertNotNull(fishBoxClass);
29+
30+
// Base type property getter
31+
Method getCount = fishBoxClass.getMethod("getCount");
32+
assertNotNull(getCount);
33+
assertEquals(long.class, getCount.getReturnType());
34+
35+
// Base type property setter
36+
Method setCount = fishBoxClass.getMethod("setCount", long.class);
37+
assertNotNull(setCount);
38+
39+
// Constrained extension method (only on FishBox, not on Box)
40+
Method describeFish = fishBoxClass.getMethod("describeFish");
41+
assertNotNull(describeFish);
42+
assertEquals(String.class, describeFish.getReturnType());
43+
}
44+
45+
@Test
46+
void fishBoxDoesNotHaveGenericTypeParameter() {
47+
// FishBox is a concrete specialization — no generic type parameters
48+
assertEquals(0, FishBox.class.getTypeParameters().length,
49+
"FishBox should have no generic type parameters");
50+
}
51+
52+
@Test
53+
void boxHasGenericTypeParameter() {
54+
// Box<Element> retains its generic parameter
55+
assertEquals(1, Box.class.getTypeParameters().length,
56+
"Box should have one generic type parameter");
57+
assertEquals("Element", Box.class.getTypeParameters()[0].getName());
58+
}
59+
}

Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ extension ImplicitlyUnwrappedOptionalTypeSyntax {
3131
wrappedType: wrappedType,
3232
self.unexpectedBetweenWrappedTypeAndExclamationMark,
3333
self.unexpectedAfterExclamationMark,
34-
trailingTrivia: self.trailingTrivia
34+
trailingTrivia: self.trailingTrivia,
3535
)
3636
}
3737
}
@@ -128,7 +128,9 @@ extension WithModifiersSyntax {
128128
}
129129

130130
extension AttributeListSyntax.Element {
131-
/// Whether this node has `SwiftJava` attributes.
131+
/// Whether this node has `SwiftJava` wrapping attributes (types that wrap Java classes).
132+
/// These are skipped during jextract because they represent Java->Swift wrappers.
133+
/// Note: `@JavaExport` is NOT included here — it forces export of Swift types to Java.
132134
var isJava: Bool {
133135
guard case let .attribute(attr) = self else {
134136
// FIXME: Handle #if.
@@ -143,6 +145,14 @@ extension AttributeListSyntax.Element {
143145
return false
144146
}
145147
}
148+
149+
/// Whether this is a `@JavaExport` attribute (used on typealiases for specialization,
150+
/// or on struct/class/enum to force-include them even when excluded by filters)
151+
var isJavaExport: Bool {
152+
guard case let .attribute(attr) = self else { return false }
153+
guard let attrName = attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { return false }
154+
return attrName == "JavaExport"
155+
}
146156
}
147157

148158
extension DeclSyntaxProtocol {
@@ -260,7 +270,7 @@ extension DeclSyntaxProtocol {
260270
.with(\.accessorBlock, nil)
261271
.with(\.initializer, nil)
262272
}
263-
)
273+
),
264274
)
265275
.triviaSanitizedDescription
266276
case .enumCaseDecl(let node):

Sources/JExtractSwiftLib/ImportedDecls.swift

Lines changed: 179 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,38 +29,207 @@ package enum SwiftAPIKind: Equatable {
2929

3030
/// Describes a Swift nominal type (e.g., a class, struct, enum) that has been
3131
/// imported and is being translated into Java.
32+
///
33+
/// When `base` is non-nil, this is a specialization of a generic type
34+
/// (e.g. `FishBox` specializing `Box<Element>` with `Element` = `Fish`).
35+
/// The specialization delegates its member collections to the base type
36+
/// so that extensions discovered later are visible through all specializations.
3237
package final class ImportedNominalType: ImportedDecl {
3338
let swiftNominal: SwiftNominalTypeDeclaration
3439

40+
/// If this type is a specialization (FishTank), then this points at the Tank base type of the specialization.
41+
/// His allows simplified
42+
package let specializationBaseType: ImportedNominalType?
43+
3544
// The short path from module root to the file in which this nominal was originally declared.
3645
// E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`.
3746
package var sourceFilePath: String {
3847
self.swiftNominal.sourceFilePath
3948
}
4049

41-
package var initializers: [ImportedFunc] = []
42-
package var methods: [ImportedFunc] = []
43-
package var variables: [ImportedFunc] = []
44-
package var cases: [ImportedEnumCase] = []
45-
var inheritedTypes: [SwiftType]
46-
package var parent: SwiftNominalTypeDeclaration?
50+
// Backing storage for member collections
51+
private var _initializers: [ImportedFunc] = []
52+
private var _methods: [ImportedFunc] = []
53+
private var _variables: [ImportedFunc] = []
54+
private var _cases: [ImportedEnumCase] = []
55+
private var _inheritedTypes: [SwiftType]
56+
private var _parent: SwiftNominalTypeDeclaration?
57+
58+
// Additional members from constrained extensions that only apply to this specialization
59+
package var constrainedInitializers: [ImportedFunc] = []
60+
package var constrainedMethods: [ImportedFunc] = []
61+
package var constrainedVariables: [ImportedFunc] = []
62+
63+
package var initializers: [ImportedFunc] {
64+
get {
65+
if let specializationBaseType { specializationBaseType.initializers + constrainedInitializers } else { _initializers }
66+
}
67+
set {
68+
if let specializationBaseType {
69+
let baseSet = Set(specializationBaseType.initializers.map { ObjectIdentifier($0) })
70+
constrainedInitializers = newValue.filter { !baseSet.contains(ObjectIdentifier($0)) }
71+
} else {
72+
_initializers = newValue
73+
}
74+
}
75+
}
76+
package var methods: [ImportedFunc] {
77+
get {
78+
if let specializationBaseType { specializationBaseType.methods + constrainedMethods } else { _methods }
79+
}
80+
set {
81+
if let specializationBaseType {
82+
let baseSet = Set(specializationBaseType.methods.map { ObjectIdentifier($0) })
83+
constrainedMethods = newValue.filter { !baseSet.contains(ObjectIdentifier($0)) }
84+
} else {
85+
_methods = newValue
86+
}
87+
}
88+
}
89+
package var variables: [ImportedFunc] {
90+
get {
91+
if let specializationBaseType { specializationBaseType.variables + constrainedVariables } else { _variables }
92+
}
93+
set {
94+
if let specializationBaseType {
95+
let baseSet = Set(specializationBaseType.variables.map { ObjectIdentifier($0) })
96+
constrainedVariables = newValue.filter { !baseSet.contains(ObjectIdentifier($0)) }
97+
} else {
98+
_variables = newValue
99+
}
100+
}
101+
}
102+
package var cases: [ImportedEnumCase] {
103+
get {
104+
if let specializationBaseType { specializationBaseType.cases } else { _cases }
105+
}
106+
set {
107+
if let specializationBaseType { specializationBaseType.cases = newValue } else { _cases = newValue }
108+
}
109+
}
110+
var inheritedTypes: [SwiftType] {
111+
get {
112+
if let specializationBaseType { specializationBaseType.inheritedTypes } else { _inheritedTypes }
113+
}
114+
set {
115+
if let specializationBaseType { specializationBaseType.inheritedTypes = newValue } else { _inheritedTypes = newValue }
116+
}
117+
}
118+
package var parent: SwiftNominalTypeDeclaration? {
119+
get {
120+
if let specializationBaseType { specializationBaseType.parent } else { _parent }
121+
}
122+
set {
123+
if let specializationBaseType { specializationBaseType.parent = newValue } else { _parent = newValue }
124+
}
125+
}
126+
127+
/// The Swift base type name, e.g. "Box" — always the unparameterized name
128+
package var baseTypeName: String { swiftNominal.qualifiedName }
129+
130+
/// The specialized/Java-facing name, e.g. "FishBox" — nil for base types
131+
package private(set) var specializedTypeName: String?
132+
133+
/// Whether this type is a specialization of a generic type
134+
package var isSpecialization: Bool { specializationBaseType != nil }
135+
136+
/// Generic parameter names (e.g. ["Element"] for Box<Element>). Empty for non-generic types
137+
package var genericParameterNames: [String] {
138+
swiftNominal.genericParameters.map(\.name)
139+
}
140+
141+
/// Maps generic parameter -> concrete type argument. Empty for unspecialized types
142+
/// e.g. {"Element": "Fish"} for FishBox
143+
package var genericArguments: [String: String] = [:]
144+
145+
/// True when all generic parameters have corresponding arguments
146+
package var isFullySpecialized: Bool {
147+
!genericParameterNames.isEmpty && genericParameterNames.allSatisfy { genericArguments.keys.contains($0) }
148+
}
47149

48150
init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws {
49151
self.swiftNominal = swiftNominal
50-
self.inheritedTypes =
152+
self.specializationBaseType = nil
153+
self._inheritedTypes =
51154
swiftNominal.inheritanceTypes?.compactMap {
52155
try? SwiftType($0.type, lookupContext: lookupContext)
53156
} ?? []
54-
self.parent = swiftNominal.parent
157+
self._parent = swiftNominal.parent
158+
}
159+
160+
/// Init for creating a specialization
161+
private init(base: ImportedNominalType, specializedTypeName: String, genericArguments: [String: String]) {
162+
self.swiftNominal = base.swiftNominal
163+
self.specializationBaseType = base
164+
self.specializedTypeName = specializedTypeName
165+
self.genericArguments = genericArguments
166+
self._inheritedTypes = []
55167
}
56168

57169
var swiftType: SwiftType {
58170
.nominal(.init(nominalTypeDecl: swiftNominal))
59171
}
60172

173+
/// The effective Java-facing name — "FishBox" for specialized, "Box" for base
174+
var effectiveJavaName: String {
175+
specializedTypeName ?? swiftNominal.qualifiedName
176+
}
177+
178+
/// The simple Java class name (no qualification) for file naming purposes
179+
var effectiveJavaSimpleName: String {
180+
specializedTypeName ?? swiftNominal.name
181+
}
182+
183+
/// The Swift type for thunk generation — "Box<Fish>" for specialized, "Box" for base
184+
/// Computed from baseTypeName + genericArguments
185+
var effectiveSwiftTypeName: String {
186+
guard !genericArguments.isEmpty else { return baseTypeName }
187+
let orderedArgs = genericParameterNames.compactMap { genericArguments[$0] }
188+
guard !orderedArgs.isEmpty else { return baseTypeName }
189+
return "\(baseTypeName)<\(orderedArgs.joined(separator: ", "))>"
190+
}
191+
61192
var qualifiedName: String {
62193
self.swiftNominal.qualifiedName
63194
}
195+
196+
/// The Java generic clause, e.g. "<Element>" for generic base types, "" for specialized or non-generic
197+
var javaGenericClause: String {
198+
if isSpecialization {
199+
""
200+
} else if genericParameterNames.isEmpty {
201+
""
202+
} else {
203+
"<\(genericParameterNames.joined(separator: ", "))>"
204+
}
205+
}
206+
207+
/// Create a specialized version of this generic type
208+
package func specialize(
209+
as specializedName: String,
210+
with substitutions: [String: String],
211+
) throws -> ImportedNominalType {
212+
guard !genericParameterNames.isEmpty else {
213+
throw SpecializationError(
214+
message: "Unable to specialize non-generic type '\(baseTypeName)' as '\(specializedName)'"
215+
)
216+
}
217+
let missingParams = genericParameterNames.filter { substitutions[$0] == nil }
218+
guard missingParams.isEmpty else {
219+
throw SpecializationError(
220+
message: "Missing type arguments for: \(missingParams) when specializing \(baseTypeName) as \(specializedName)"
221+
)
222+
}
223+
return ImportedNominalType(
224+
base: self,
225+
specializedTypeName: specializedName,
226+
genericArguments: substitutions,
227+
)
228+
}
229+
}
230+
231+
struct SpecializationError: Error {
232+
let message: String
64233
}
65234

66235
public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible {
@@ -82,7 +251,7 @@ public final class ImportedEnumCase: ImportedDecl, CustomStringConvertible {
82251
parameters: [SwiftEnumCaseParameter],
83252
swiftDecl: any DeclSyntaxProtocol,
84253
enumType: SwiftNominalType,
85-
caseFunction: ImportedFunc
254+
caseFunction: ImportedFunc,
86255
) {
87256
self.name = name
88257
self.parameters = parameters
@@ -191,7 +360,7 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible {
191360
swiftDecl: any DeclSyntaxProtocol,
192361
name: String,
193362
apiKind: SwiftAPIKind,
194-
functionSignature: SwiftFunctionSignature
363+
functionSignature: SwiftFunctionSignature,
195364
) {
196365
self.module = module
197366
self.name = name

Sources/JExtractSwiftLib/JNI/JNICaching.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
enum JNICaching {
1616
static func cacheName(for type: ImportedNominalType) -> String {
17-
cacheName(for: type.swiftNominal.qualifiedName)
17+
cacheName(for: type.effectiveJavaName)
1818
}
1919

2020
static func cacheName(for type: SwiftNominalType) -> String {

0 commit comments

Comments
 (0)