Skip to content

[JExtract/JNI] Add support for classes/structs as parameters and return values #326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 22, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -13,8 +13,8 @@
//===----------------------------------------------------------------------===//

public class MySwiftClass {
let x: Int64
let y: Int64
public let x: Int64
public let y: Int64

public let byte: UInt8 = 0
public let constant: Int64 = 100
Expand Down Expand Up @@ -76,4 +76,12 @@ public class MySwiftClass {
public func throwingFunction() throws {
throw MySwiftClassError.swiftError
}

public func sumX(with other: MySwiftClass) -> Int64 {
return self.x + other.x
}

public func copy() -> MySwiftClass {
return MySwiftClass(x: self.x, y: self.y)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,25 @@ void isWarm() {
assertFalse(c.isWarm());
}
}

@Test
void sumWithX() {
try (var arena = new ConfinedSwiftMemorySession()) {
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
MySwiftClass c2 = MySwiftClass.init(50, 10, arena);
assertEquals(70, c1.sumX(c2));
}
}

@Test
void copy() {
try (var arena = new ConfinedSwiftMemorySession()) {
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
MySwiftClass c2 = c1.copy(arena);

assertEquals(20, c2.getX());
assertEquals(10, c2.getY());
assertNotEquals(c1.$memoryAddress(), c2.$memoryAddress());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ extension JNISwift2JavaGenerator {
printer.println()

for initializer in decl.initializers {
printInitializerBindings(&printer, initializer, type: decl)
printFunctionBinding(&printer, initializer)
printer.println()
}

Expand Down Expand Up @@ -176,75 +176,80 @@ extension JNISwift2JavaGenerator {
}

private func printFunctionBinding(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
guard let _ = translatedDecl(for: decl) else {
guard let translatedDecl = translatedDecl(for: decl) else {
// Failed to translate. Skip.
return
}

var modifiers = "public"
if decl.isStatic || decl.isInitializer || !decl.hasParent {
printStaticFunctionBinding(&printer, decl)
} else {
printMemberMethodBindings(&printer, decl)
modifiers.append(" static")
}
}

private func printStaticFunctionBinding(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
printDeclDocumentation(&printer, decl)
printer.print(
"public static native \(renderFunctionSignature(decl));"
)
}

/// Renders Java bindings for member methods
///
/// Member methods are generated as a function that extracts the `selfPointer`
/// and passes it down to another native function along with the arguments
/// to call the Swift implementation.
private func printMemberMethodBindings(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
let translatedDecl = translatedDecl(for: decl)! // We will only call this method if we can translate the decl.
let translatedSignature = translatedDecl.translatedFunctionSignature
let resultType = translatedSignature.resultType.javaType
var parameters = translatedDecl.translatedFunctionSignature.parameters.map(\.parameter.asParameter)
if translatedSignature.requiresSwiftArena {
parameters.append("SwiftArena swiftArena$")
}
let throwsClause = decl.isThrowing ? " throws Exception" : ""

printDeclDocumentation(&printer, decl)
printer.printBraceBlock("public \(renderFunctionSignature(decl))") { printer in
var arguments = translatedDecl.translatedFunctionSignature.parameters.map(\.name)

let selfVarName = "self$"
arguments.append(selfVarName)
printer.printBraceBlock(
"\(modifiers) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)"
) { printer in
printDowncall(&printer, decl)
}

let returnKeyword = translatedDecl.translatedFunctionSignature.resultType.isVoid ? "" : "return "
printNativeFunction(&printer, decl)
}

printer.print(
"""
long \(selfVarName) = this.$memoryAddress();
\(returnKeyword)\(translatedDecl.parentName).$\(translatedDecl.name)(\(arguments.joined(separator: ", ")));
"""
)
private func printNativeFunction(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
let translatedDecl = translatedDecl(for: decl)! // Will always call with valid decl
let nativeSignature = translatedDecl.nativeFunctionSignature
let resultType = nativeSignature.result.javaType
let nativeName = "$\(translatedDecl.name)"
var parameters = nativeSignature.parameters
if let selfParameter = nativeSignature.selfParameter {
parameters.append(selfParameter)
}
let renderedParameters = parameters.map { "\($0.javaParameter.type) \($0.javaParameter.name)"}.joined(separator: ", ")

let returnType = translatedDecl.translatedFunctionSignature.resultType
var parameters = translatedDecl.translatedFunctionSignature.parameters.map(\.asParameter)
parameters.append("long selfPointer")
printer.print("private static native \(returnType) $\(translatedDecl.name)(\(parameters.joined(separator: ", ")));")
printer.print("private static native \(resultType) \(nativeName)(\(renderedParameters));")
}

private func printInitializerBindings(_ printer: inout CodePrinter, _ decl: ImportedFunc, type: ImportedNominalType) {
guard let translatedDecl = translatedDecl(for: decl) else {
// Failed to translate. Skip.
return
private func printDowncall(
_ printer: inout CodePrinter,
_ decl: ImportedFunc
) {
let translatedDecl = translatedDecl(for: decl)! // We will only call this method if we can translate the decl.
let translatedFunctionSignature = translatedDecl.translatedFunctionSignature

// Regular parameters.
var arguments = [String]()
for parameter in translatedFunctionSignature.parameters {
let lowered = parameter.conversion.render(&printer, parameter.parameter.name)
arguments.append(lowered)
}

printDeclDocumentation(&printer, decl)
printer.printBraceBlock("public static \(renderFunctionSignature(decl))") { printer in
let initArguments = translatedDecl.translatedFunctionSignature.parameters.map(\.name)
printer.print(
"""
long self$ = \(type.qualifiedName).allocatingInit(\(initArguments.joined(separator: ", ")));
return new \(type.qualifiedName)(self$, swiftArena$);
"""
)
// 'self' parameter.
if let selfParameter = translatedFunctionSignature.selfParameter {
let lowered = selfParameter.conversion.render(&printer, "this")
arguments.append(lowered)
}

let parameters = translatedDecl.translatedFunctionSignature.parameters.map(\.asParameter)
printer.print("private static native long allocatingInit(\(parameters.joined(separator: ", ")));")
//=== Part 3: Downcall.
// TODO: If we always generate a native method and a "public" method, we can actually choose our own thunk names
// using the registry?
let downcall = "\(translatedDecl.parentName).$\(translatedDecl.name)(\(arguments.joined(separator: ", ")))"

//=== Part 4: Convert the return value.
if translatedFunctionSignature.resultType.javaType.isVoid {
printer.print("\(downcall);")
} else {
let result = translatedFunctionSignature.resultType.conversion.render(&printer, downcall)
printer.print("return \(result);")
}
}

private func printDeclDocumentation(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
Expand Down Expand Up @@ -288,24 +293,4 @@ extension JNISwift2JavaGenerator {
)
}
}

/// Renders a Java function signature
///
/// `func method(x: Int, y: Int) -> Int` becomes
/// `long method(long x, long y)`
private func renderFunctionSignature(_ decl: ImportedFunc) -> String {
guard let translatedDecl = translatedDecl(for: decl) else {
fatalError("Unable to render function signature for a function that cannot be translated: \(decl)")
}
let resultType = translatedDecl.translatedFunctionSignature.resultType
var parameters = translatedDecl.translatedFunctionSignature.parameters.map(\.asParameter)

if decl.isInitializer {
parameters.append("SwiftArena swiftArena$")
}

let throwsClause = decl.isThrowing ? " throws Exception" : ""

return "\(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)"
}
}
Loading
Loading