Skip to content

[JExtract] Add support for JNI member methods #305

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 2 commits into from
Jul 12, 2025
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 @@ -34,4 +34,20 @@ public class MySwiftClass {
deinit {
p("deinit called!")
}

public func sum() -> Int64 {
return x + y
}

public func xMultiplied(by z: Int64) -> Int64 {
return x * z;
}

enum MySwiftClassError: Error {
case swiftError
}

public func throwingFunction() throws {
throw MySwiftClassError.swiftError
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ static void examples() {
try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) {
MySwiftClass myClass = MySwiftClass.init(10, 5, arena);
MySwiftClass myClass2 = MySwiftClass.init(arena);

try {
myClass.throwingFunction();
} catch (Exception e) {
System.out.println("Caught exception: " + e.getMessage());
}
}

System.out.println("DONE.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,30 @@ void init_withParameters() {
assertNotNull(c);
}
}

@Test
void sum() {
try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) {
MySwiftClass c = MySwiftClass.init(20, 10, arena);
assertEquals(30, c.sum());
}
}

@Test
void xMultiplied() {
try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) {
MySwiftClass c = MySwiftClass.init(20, 10, arena);
assertEquals(200, c.xMultiplied(10));
}
}

@Test
void throwingFunction() {
try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) {
MySwiftClass c = MySwiftClass.init(20, 10, arena);
Exception exception = assertThrows(Exception.class, () -> c.throwingFunction());

assertEquals("swiftError", exception.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,49 @@ extension JNISwift2JavaGenerator {
}

private func printFunctionBinding(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
if decl.isStatic || decl.isInitializer || !decl.hasParent {
printStaticFunctionBinding(&printer, decl)
} else {
printMemberMethodBindings(&printer, decl)
}
}

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)

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

let returnKeyword = translatedDecl.translatedFunctionSignature.resultType.isVoid ? "" : "return "

printer.print(
"""
long selfPointer = this.pointer();
\(returnKeyword)\(translatedDecl.parentName).$\(translatedDecl.name)(\(arguments.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: ", ")));")
}

private func printInitializerBindings(_ printer: inout CodePrinter, _ decl: ImportedFunc, type: ImportedNominalType) {
let translatedDecl = translatedDecl(for: decl)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,67 +136,97 @@ extension JNISwift2JavaGenerator {
_ printer: inout CodePrinter,
_ decl: ImportedFunc
) {
// Free functions does not have a parent
if decl.isStatic || !decl.hasParent {
self.printSwiftStaticFunctionThunk(&printer, decl)
} else {
self.printSwiftMemberFunctionThunk(&printer, decl)
}
}

private func printSwiftStaticFunctionThunk(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
let translatedDecl = self.translatedDecl(for: decl)
let parentName = translatedDecl.parentName
let swiftReturnType = decl.functionSignature.result.type

printCDecl(&printer, decl) { printer in
let downcallParameters = renderDowncallArguments(
swiftFunctionSignature: decl.functionSignature,
translatedFunctionSignature: translatedDecl.translatedFunctionSignature
printCDecl(
&printer,
javaMethodName: translatedDecl.name,
parentName: translatedDecl.parentName,
parameters: translatedDecl.translatedFunctionSignature.parameters,
isStatic: true,
resultType: translatedDecl.translatedFunctionSignature.resultType
) { printer in
// For free functions the parent is the Swift module
let parentName = decl.parentType?.asNominalTypeDeclaration?.qualifiedName ?? swiftModuleName
self.printFunctionDowncall(&printer, decl, calleeName: parentName)
}
}

private func printSwiftMemberFunctionThunk(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
let translatedDecl = self.translatedDecl(for: decl)
let swiftParentName = decl.parentType!.asNominalTypeDeclaration!.qualifiedName

printCDecl(
&printer,
javaMethodName: "$\(translatedDecl.name)",
parentName: translatedDecl.parentName,
parameters: translatedDecl.translatedFunctionSignature.parameters + [
JavaParameter(name: "selfPointer", type: .long)
],
isStatic: true,
resultType: translatedDecl.translatedFunctionSignature.resultType
) { printer in
printer.print(
"""
let self$ = UnsafeMutablePointer<\(swiftParentName)>(bitPattern: Int(Int64(fromJNI: selfPointer, in: environment!)))!
"""
)
let tryClause: String = decl.isThrowing ? "try " : ""
let functionDowncall =
"\(tryClause)\(parentName).\(decl.name)(\(downcallParameters))"

let innerBody =
if swiftReturnType.isVoid {
functionDowncall
} else {
"""
let result = \(functionDowncall)
return result.getJNIValue(in: environment)
"""
}
self.printFunctionDowncall(&printer, decl, calleeName: "self$.pointee")
}
}

private func printFunctionDowncall(
_ printer: inout CodePrinter,
_ decl: ImportedFunc,
calleeName: String
) {
let translatedDecl = self.translatedDecl(for: decl)
let swiftReturnType = decl.functionSignature.result.type

let downcallParameters = renderDowncallArguments(
swiftFunctionSignature: decl.functionSignature,
translatedFunctionSignature: translatedDecl.translatedFunctionSignature
)
let tryClause: String = decl.isThrowing ? "try " : ""
let functionDowncall = "\(tryClause)\(calleeName).\(decl.name)(\(downcallParameters))"

let returnStatement =
if swiftReturnType.isVoid {
functionDowncall
} else {
"""
let result = \(functionDowncall)
return result.getJNIValue(in: environment)
"""
}

if decl.isThrowing {
let dummyReturn =
!swiftReturnType.isVoid ? "return \(swiftReturnType).jniPlaceholderValue" : ""
printer.print(
if decl.isThrowing {
let dummyReturn =
!swiftReturnType.isVoid ? "return \(swiftReturnType).jniPlaceholderValue" : ""
printer.print(
"""
do {
\(innerBody)
\(returnStatement)
} catch {
environment.throwAsException(error)
\(dummyReturn)
}
"""
)
} else {
printer.print(innerBody)
}
)
} else {
printer.print(returnStatement)
}
}

private func printCDecl(
_ printer: inout CodePrinter,
_ decl: ImportedFunc,
_ body: (inout CodePrinter) -> Void
) {
let translatedDecl = translatedDecl(for: decl)
let parentName = translatedDecl.parentName

printCDecl(
&printer,
javaMethodName: translatedDecl.name,
parentName: parentName,
parameters: translatedDecl.translatedFunctionSignature.parameters,
isStatic: decl.isStatic || decl.isInitializer || !decl.hasParent,
resultType: translatedDecl.translatedFunctionSignature.resultType,
body
)
}

private func printCDecl(
_ printer: inout CodePrinter,
javaMethodName: String,
Expand Down
2 changes: 1 addition & 1 deletion Sources/JavaKit/Exceptions/ExceptionHandling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ extension JNIEnvironment {
}

// Otherwise, create a exception with a message.
_ = try! JavaClass<Exception>.withJNIClass(in: self) { exceptionClass in
_ = try! Exception.withJNIClass(in: self) { exceptionClass in
interface.ThrowNew(self, exceptionClass, String(describing: error))
}
}
Expand Down
47 changes: 47 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIClassTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ struct JNIClassTests {
self.x = 0
self.y = 0
}

public func doSomething(x: Int64) {}
}
"""

Expand Down Expand Up @@ -206,4 +208,49 @@ struct JNIClassTests {
]
)
}

@Test
func memberMethod_javaBindings() throws {
try assertOutput(
input: source,
.jni,
.java,
expectedChunks: [
"""
/**
* Downcall to Swift:
* {@snippet lang=swift :
* public func doSomething(x: Int64)
* }
*/
public void doSomething(long x) {
long selfPointer = this.pointer();
MyClass.$doSomething(x, selfPointer);
}
""",
"""
private static native void $doSomething(long x, long selfPointer);
"""
]
)
}

@Test
func memberMethod_swiftThunks() throws {
try assertOutput(
input: source,
.jni,
.swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
@_cdecl("Java_com_example_swift_MyClass__00024doSomething__JJ")
func Java_com_example_swift_MyClass__00024doSomething__JJ(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, x: jlong, selfPointer: jlong) {
let self$ = UnsafeMutablePointer<MyClass>(bitPattern: Int(Int64(fromJNI: selfPointer, in: environment!)))!
self$.pointee.doSomething(x: Int64(fromJNI: x, in: environment!))
}
""",
]
)
}
}
Loading