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
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ public class MySwiftClass {
MySwiftClass(x: x, y: y)
}

public class func classMethod(x: Int) -> Int {
x * 2
}

public class var classVariable: Int {
42
}

public func sum() -> Int {
x + y
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ void test_MySwiftClass_describe() {
}
}

@Test
void test_MySwiftClass_classMembers() {
// snippet.classMembersUsageJava
assertEquals(24, MySwiftClass.classMethod(12));
assertEquals(42, MySwiftClass.getClassVariable());
// snippet.end
}

@Test
@Disabled // TODO: Need var mangled names in interfaces
void test_MySwiftClass_property_x() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ public class MySwiftClass {
public static func method() {
}

// snippet.classMembers
public class func classMethod(x: Int64) -> Int64 {
x * 2
}

public class var classVariable: Int64 {
42
}
// snippet.end

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding snippets but we're not using them it seems -- can you add a small bit in FeaturesJextract.md around static methods somewhere:

+### Class methods
+
+Swift `class` methods and computed properties are imported as `static` methods on the generated Java wrapper type.
+
+@TabNavigator {
+   @Tab("Swift") {
+      @Snippet(path: "Snippets/ClassesSwift.swift", slice: "classMembers")
+   }
+   @Tab("Java (JNI)") {
+      @Snippet(path: "Snippets/MySwiftClassTest", slice: "classMembers")
+   }
+   @Tab("Java (FFM)") {
+      @Snippet(path: "Snippets/MySwiftClassTest", slice: "classMembers")
+   }
+}

And make sure we have the snippets for the usage as well


// snippet.initializers
public init(x: Int64, y: Int64) {
self.x = x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ void sum() {
// snippet.end
}

@Test
void classMembers() {
// snippet.classMembersUsageJava
assertEquals(24, MySwiftClass.classMethod(12));
assertEquals(42, MySwiftClass.getClassVariable());
// snippet.end
}

@Test
void xMultiplied() {
try (var arena = SwiftArena.ofConfined()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ struct CdeclLowering {
genericParameters: signature.genericParameters,
genericRequirements: signature.genericRequirements,
)
case nil, .initializer(_), .staticMethod(_):
case nil, .initializer(_), .staticMethod(_), .classMethod(_):
nil
}

Expand Down Expand Up @@ -1005,7 +1005,7 @@ extension LoweredFunctionSignature {
placeholder: "self",
bodyItems: &bodyItems,
)
case .staticMethod(let selfType), .initializer(let selfType):
case .staticMethod(let selfType), .classMethod(let selfType), .initializer(let selfType):
selfExpr = "\(raw: selfType.description)"
case .none:
selfExpr = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ extension FFMSwift2JavaGenerator {

var modifiers = "public"
switch decl.functionSignature.selfParameter {
case .staticMethod, .initializer, nil:
case .staticMethod, .classMethod, .initializer, nil:
modifiers.append(" static")
default:
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ extension JNISwift2JavaGenerator {
}

for method in decl.methods {
if isEffectivelyGeneric && method.isStatic {
if isEffectivelyGeneric && (method.isStatic || method.isClass) {
self.logger.debug("Skipping static method '\(method.name)' on unspecialized generic type '\(decl.effectiveJavaName)'")
continue
}
Expand All @@ -421,7 +421,7 @@ extension JNISwift2JavaGenerator {
}

for variable in decl.variables {
if isEffectivelyGeneric && variable.isStatic {
if isEffectivelyGeneric && (variable.isStatic || variable.isClass) {
self.logger.debug("Skipping static property '\(variable.name)' on unspecialized generic type '\(decl.effectiveJavaName)'")
continue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ extension JNISwift2JavaGenerator {

return TranslatedFunctionDecl(
name: javaName,
isStatic: decl.isStatic || !decl.hasParent || decl.isInitializer,
isStatic: decl.isStatic || decl.isClass || !decl.hasParent || decl.isInitializer,
isThrowing: decl.isThrowing,
isAsync: decl.isAsync,
isIsolated: decl.isIsolated,
Expand Down Expand Up @@ -391,7 +391,7 @@ extension JNISwift2JavaGenerator {
parameter: JavaParameter(name: "selfTypePointer", type: .long),
conversion: .typeMetadataAddress(.placeholder),
)
case .staticMethod, .initializer:
case .staticMethod, .classMethod, .initializer:
return TranslatedParameter(
parameter: JavaParameter(name: "selfTypePointer", type: .long),
conversion: .constant("$typeMetadataAddressDowncall()"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ extension JNISwift2JavaGenerator {
genericRequirements: functionSignature.genericRequirements
)
}
case nil, .initializer(_), .staticMethod(_):
case nil, .initializer(_), .staticMethod(_), .classMethod(_):
nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ extension JNISwift2JavaGenerator {
&printer,
"selfPointer",
)
case .staticMethod(let selfType), .initializer(let selfType):
case .staticMethod(let selfType), .classMethod(let selfType), .initializer(let selfType):
"\(selfType)"
case .none:
swiftModuleName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ extension JNISwift2JavaGenerator {
/// doesn't translate (e.g. referencing `Self`/associated types).
func supportedProtocolRequirements(of type: ExtractedNominalType) -> [ExtractedFunc] {
(type.methods + type.variables).filter { requirement in
!requirement.isStatic && self.translatedDecl(for: requirement) != nil
!requirement.isStatic && !requirement.isClass && self.translatedDecl(for: requirement) != nil
}
}

Expand Down
7 changes: 7 additions & 0 deletions Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,13 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible {
return false
}

public var isClass: Bool {
if case .classMethod = functionSignature.selfParameter {
return true
}
return false
}

public var isInitializer: Bool {
if case .initializer = functionSignature.selfParameter {
return true
Expand Down
20 changes: 16 additions & 4 deletions Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,19 @@ public enum SwiftSelfParameter: Equatable {
/// form the call.
case staticMethod(SwiftType)

/// 'self' is the type for a call to a class method. We only need the type
/// to form the call.
case classMethod(SwiftType)

/// 'self' is the type for a call to an initializer. We only need the type
/// to form the call.
case initializer(SwiftType)

public var selfType: SwiftType {
get {
switch self {
case .instance(_, let swiftType), .staticMethod(let swiftType), .initializer(let swiftType):
case .instance(_, let swiftType), .staticMethod(let swiftType), .classMethod(let swiftType),
.initializer(let swiftType):
return swiftType
}
}
Expand All @@ -91,6 +96,8 @@ public enum SwiftSelfParameter: Equatable {
self = .instance(convention: convention, swiftType: newValue)
case .staticMethod:
self = .staticMethod(newValue)
case .classMethod:
self = .classMethod(newValue)
case .initializer:
self = .initializer(newValue)
}
Expand Down Expand Up @@ -173,18 +180,21 @@ extension SwiftFunctionSignature {
var isMutating = false
var isConsuming = false
var isStatic = false
var isClass = false
for modifier in node.modifiers {
switch modifier.name.tokenKind {
case .keyword(.mutating): isMutating = true
case .keyword(.static): isStatic = true
case .keyword(.class): isClass = true
case .keyword(.consuming): isConsuming = true
case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name)
default: break
}
}

if isStatic {
selfParameter = .staticMethod(enclosingType)
} else if isClass {
selfParameter = .classMethod(enclosingType)
} else {
selfParameter = .instance(
convention: isMutating ? .inout : isConsuming ? .consuming : .byValue,
Expand Down Expand Up @@ -437,16 +447,19 @@ extension SwiftFunctionSignature {
// type and convention for the self parameter.
if let enclosingType {
var isStatic = false
var isClass = false
for modifier in modifiers {
switch modifier.name.tokenKind {
case .keyword(.static): isStatic = true
case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name)
case .keyword(.class): isClass = true
default: break
}
}

if isStatic {
return .staticMethod(enclosingType)
} else if isClass {
return .classMethod(enclosingType)
} else {
return .instance(
convention: isSet && !enclosingType.isReferenceType ? .inout : .byValue,
Expand Down Expand Up @@ -524,7 +537,6 @@ extension AccessorBlockSyntax {
public enum SwiftFunctionTranslationError: Error {
case `throws`(ThrowsClauseSyntax)
case async(TokenSyntax)
case classMethod(TokenSyntax)
case missingEnclosingType(InitializerDeclSyntax)
case failableInitializer(InitializerDeclSyntax)
case multipleBindings(VariableDeclSyntax)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ For guidance on choosing between JNI and FFM mode, see <doc:FeaturesOverview>.

Classes and structs can both have initializers imported.

### Class members

Swift `class` methods and computed properties are imported as `static` methods on the generated Java wrapper type.

@TabNavigator {
@Tab("Swift") {
@Snippet(path: "Snippets/ClassesSwift.swift", slice: "classMembers")
}
@Tab("Java (JNI)") {
@Snippet(path: "Snippets/ClassesJavaJNI", slice: "classMembersUsageJava")
}
@Tab("Java (FFM)") {
@Snippet(path: "Snippets/ClassesJavaFFM", slice: "classMembersUsageJava")
}
}

### Optional initializers / Throwing initializers

Optional and throwing initializers are supported in JNI mode.
Expand Down
94 changes: 94 additions & 0 deletions Tests/JExtractSwiftTests/FFM/FFMClassMemberTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//===----------------------------------------------------------------------===//
//
// 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 JExtractSwiftLib
import Testing

@Suite
struct FFMClassMemberTests {
let source = """
public class MyClass {
public class func classMethod() -> Int64 { 42 }
public class var classVariable: Int64 { 7 }
}
"""

@Test
func classMethod_javaBindings() throws {
try assertOutput(
input: source,
.ffm,
.java,
expectedChunks: [
"""
/**
* Downcall to Swift:
* {@snippet lang=swift :
* public class func classMethod() -> Int64
* }
*/
public static long classMethod() {
return swiftjava_SwiftModule_MyClass_classMethod.call();
}
"""
]
)
}

@Test
func classVariable_javaBindings() throws {
try assertOutput(
input: source,
.ffm,
.java,
expectedChunks: [
"""
/**
* Downcall to Swift:
* {@snippet lang=swift :
* public class var classVariable: Int64
* }
*/
public static long getClassVariable() {
return swiftjava_SwiftModule_MyClass_classVariable$get.call();
}
"""
]
)
}

@Test
func classMembers_swiftThunks() throws {
try assertOutput(
input: source,
.ffm,
.swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
@_cdecl("swiftjava_SwiftModule_MyClass_classMethod")
public func swiftjava_SwiftModule_MyClass_classMethod() -> Int64 {
return MyClass.classMethod()
}
""",
"""
@_cdecl("swiftjava_SwiftModule_MyClass_classVariable$get")
public func swiftjava_SwiftModule_MyClass_classVariable$get() -> Int64 {
return MyClass.classVariable
}
""",
]
)
}
}
Loading
Loading