Skip to content

[JExtract] Generate JNI code for memory management with SwiftKitCore #302

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 11, 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 @@ -19,6 +19,7 @@
// Import javakit/swiftkit support libraries

import org.swift.swiftkit.core.SwiftLibraries;
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;

public class HelloJava2SwiftJNI {

Expand All @@ -40,8 +41,10 @@ static void examples() {

MySwiftClass.method();

MySwiftClass myClass = MySwiftClass.init(10, 5);
MySwiftClass myClass2 = MySwiftClass.init();
try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpick that this may want to be SwiftArena.ofConfined()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agreed, but the problem is that Java 7 does not support static methods on interfaces.

Copy link
Collaborator

@ktoso ktoso Jul 11, 2025

Choose a reason for hiding this comment

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

Ugh... heh I wonder if we need to really support so long back, we'll see as we chat with android workgroup.

Otherwise the Java pattern is to do a type like SwiftArenas that's a class and put the static methods on that

Copy link
Collaborator

Choose a reason for hiding this comment

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

At least we can make a () constructor that gets the current thread as well then, we can do this in a follow up

MySwiftClass myClass = MySwiftClass.init(10, 5, arena);
MySwiftClass myClass2 = MySwiftClass.init(arena);
}

System.out.println("DONE.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,25 @@

package com.example.swift;

import com.example.swift.MySwiftLibrary;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;

import static org.junit.jupiter.api.Assertions.*;

public class MySwiftClassTest {
@Test
void init_noParameters() {
MySwiftClass c = MySwiftClass.init();
assertNotNull(c);
try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) {
MySwiftClass c = MySwiftClass.init(arena);
assertNotNull(c);
}
}

@Test
void init_withParameters() {
MySwiftClass c = MySwiftClass.init(1337, 42);
assertNotNull(c);
try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) {
MySwiftClass c = MySwiftClass.init(1337, 42, arena);
assertNotNull(c);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@
//
//===----------------------------------------------------------------------===//


// MARK: Defaults

extension JNISwift2JavaGenerator {
/// Default set Java imports for every generated file
static let defaultJavaImports: Array<String> = [
"org.swift.swiftkit.core.*",
"org.swift.swiftkit.core.util.*",
]
}

// MARK: Printing

extension JNISwift2JavaGenerator {
func writeExportedJavaSources() throws {
var printer = CodePrinter()
Expand Down Expand Up @@ -72,6 +85,7 @@ extension JNISwift2JavaGenerator {
private func printImportedNominal(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
printHeader(&printer)
printPackage(&printer)
printImports(&printer)

printNominal(&printer, decl) { printer in
printer.print(
Expand All @@ -87,14 +101,10 @@ extension JNISwift2JavaGenerator {
"""
)

printer.println()

printer.print(
"""
private long selfPointer;
private \(decl.swiftNominal.name)(long selfPointer) {
this.selfPointer = selfPointer;
public \(decl.swiftNominal.name)(long selfPointer, SwiftArena swiftArena) {
super(selfPointer, swiftArena);
}
"""
)
Expand All @@ -108,6 +118,8 @@ extension JNISwift2JavaGenerator {
for method in decl.methods {
printFunctionBinding(&printer, method)
}

printDestroyFunction(&printer, decl)
}
}

Expand All @@ -130,10 +142,17 @@ extension JNISwift2JavaGenerator {
)
}

private func printImports(_ printer: inout CodePrinter) {
for i in JNISwift2JavaGenerator.defaultJavaImports {
printer.print("import \(i);")
}
printer.print("")
}

private func printNominal(
_ printer: inout CodePrinter, _ decl: ImportedNominalType, body: (inout CodePrinter) -> Void
) {
printer.printBraceBlock("public final class \(decl.swiftNominal.name)") { printer in
printer.printBraceBlock("public final class \(decl.swiftNominal.name) extends JNISwiftInstance") { printer in
body(&printer)
}
}
Expand All @@ -160,7 +179,7 @@ extension JNISwift2JavaGenerator {
printer.print(
"""
long selfPointer = \(type.qualifiedName).allocatingInit(\(initArguments.joined(separator: ", ")));
return new \(type.qualifiedName)(selfPointer);
return new \(type.qualifiedName)(selfPointer, swiftArena$);
"""
)
}
Expand All @@ -182,14 +201,39 @@ extension JNISwift2JavaGenerator {
)
}

/// Prints the destroy function for a `JNISwiftInstance`
private func printDestroyFunction(_ printer: inout CodePrinter, _ type: ImportedNominalType) {
printer.print("private static native void $destroy(long selfPointer);")

printer.print("@Override")
printer.printBraceBlock("protected Runnable $createDestroyFunction()") { printer in
printer.print(
"""
long $selfPointer = this.pointer();
return new Runnable() {
@Override
public void run() {
\(type.swiftNominal.name).$destroy($selfPointer);
}
};
"""
)
}
}

/// 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 {
let translatedDecl = translatedDecl(for: decl)
let resultType = translatedDecl.translatedFunctionSignature.resultType
let parameters = translatedDecl.translatedFunctionSignature.parameters.map(\.asParameter)
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)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
//===----------------------------------------------------------------------===//

import JavaTypes
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

extension JNISwift2JavaGenerator {
func writeSwiftThunkSources() throws {
Expand Down Expand Up @@ -96,6 +101,8 @@ extension JNISwift2JavaGenerator {
printSwiftFunctionThunk(&printer, method)
printer.println()
}

printDestroyFunctionThunk(&printer, type)
}

private func printInitializerThunk(_ printer: inout CodePrinter, _ decl: ImportedFunc) {
Expand Down Expand Up @@ -199,24 +206,18 @@ extension JNISwift2JavaGenerator {
resultType: JavaType,
_ body: (inout CodePrinter) -> Void
) {
var jniSignature = parameters.reduce(into: "") { signature, parameter in
let jniSignature = parameters.reduce(into: "") { signature, parameter in
signature += parameter.type.jniTypeSignature
}

// Escape signature characters
jniSignature = jniSignature
.replacingOccurrences(of: "_", with: "_1")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: ";", with: "_2")
.replacingOccurrences(of: "[", with: "_3")

let cName =
"Java_"
+ self.javaPackage.replacingOccurrences(of: ".", with: "_")
+ "_\(parentName)_"
+ javaMethodName
+ "_\(parentName.escapedJNIIdentifier)_"
+ javaMethodName.escapedJNIIdentifier
+ "__"
+ jniSignature
+ jniSignature.escapedJNIIdentifier

let translatedParameters = parameters.map {
"\($0.name): \($0.type.jniTypeName)"
}
Expand Down Expand Up @@ -251,6 +252,30 @@ extension JNISwift2JavaGenerator {
)
}

/// Prints the implementation of the destroy function.
private func printDestroyFunctionThunk(_ printer: inout CodePrinter, _ type: ImportedNominalType) {
printCDecl(
&printer,
javaMethodName: "$destroy",
parentName: type.swiftNominal.name,
parameters: [
JavaParameter(name: "selfPointer", type: .long)
],
isStatic: true,
resultType: .void
) { printer in
// Deinitialize the pointer allocated (which will call the VWT destroy method)
// then deallocate the memory.
printer.print(
"""
let pointer = UnsafeMutablePointer<\(type.qualifiedName)>(bitPattern: Int(Int64(fromJNI: selfPointer, in: environment!)))!
pointer.deinitialize(count: 1)
pointer.deallocate()
"""
)
}
}

/// Renders the arguments for making a downcall
private func renderDowncallArguments(
swiftFunctionSignature: SwiftFunctionSignature,
Expand All @@ -266,3 +291,29 @@ extension JNISwift2JavaGenerator {
.joined(separator: ", ")
}
}

extension String {
/// Returns a version of the string correctly escaped for a JNI
var escapedJNIIdentifier: String {
self.map {
if $0 == "_" {
return "_1"
} else if $0 == "/" {
return "_"
} else if $0 == ";" {
return "_2"
} else if $0 == "[" {
return "_3"
} else if $0.isASCII && ($0.isLetter || $0.isNumber) {
return String($0)
} else if let utf16 = $0.utf16.first {
// Escape any non-alphanumeric to their UTF16 hex encoding
let utf16Hex = String(format: "%04x", utf16)
return "_0\(utf16Hex)"
} else {
fatalError("Invalid JNI character: \($0)")
}
}
.joined()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected JNISwiftInstance(long pointer, SwiftArena arena) {
*
* @return a function that is called when the value should be destroyed.
*/
abstract Runnable $createDestroyFunction();
protected abstract Runnable $createDestroyFunction();

@Override
public SwiftInstanceCleanup createCleanupAction() {
Expand Down
54 changes: 45 additions & 9 deletions Tests/JExtractSwiftTests/JNI/JNIClassTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ struct JNIClassTests {
// Swift module: SwiftModule

package com.example.swift;

import org.swift.swiftkit.core.*;
import org.swift.swiftkit.core.util.*;

public final class MyClass {
public final class MyClass extends JNISwiftInstance {
static final String LIB_NAME = "SwiftModule";

@SuppressWarnings("unused")
Expand All @@ -55,12 +58,25 @@ struct JNIClassTests {
return true;
}

private long selfPointer;

private MyClass(long selfPointer) {
this.selfPointer = selfPointer;
public MyClass(long selfPointer, SwiftArena swiftArena) {
super(selfPointer, swiftArena);
}
""",
"""
private static native void $destroy(long selfPointer);
""",
"""
@Override
protected Runnable $createDestroyFunction() {
long $selfPointer = this.pointer();
return new Runnable() {
@Override
public void run() {
MyClass.$destroy($selfPointer);
}
};
}
"""
])
}

Expand Down Expand Up @@ -116,9 +132,9 @@ struct JNIClassTests {
* public init(x: Int64, y: Int64)
* }
*/
public static MyClass init(long x, long y) {
public static MyClass init(long x, long y, SwiftArena swiftArena$) {
long selfPointer = MyClass.allocatingInit(x, y);
return new MyClass(selfPointer);
return new MyClass(selfPointer, swiftArena$);
}
""",
"""
Expand All @@ -128,9 +144,9 @@ struct JNIClassTests {
* public init()
* }
*/
public static MyClass init() {
public static MyClass init(SwiftArena swiftArena$) {
long selfPointer = MyClass.allocatingInit();
return new MyClass(selfPointer);
return new MyClass(selfPointer, swiftArena$);
}
""",
"""
Expand Down Expand Up @@ -170,4 +186,24 @@ struct JNIClassTests {
]
)
}

@Test
func destroyFunction_swiftThunks() throws {
try assertOutput(
input: source,
.jni,
.swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
@_cdecl("Java_com_example_swift_MyClass__00024destroy__J")
func Java_com_example_swift_MyClass__00024destroy__J(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, selfPointer: jlong) {
let pointer = UnsafeMutablePointer<MyClass>(bitPattern: Int(Int64(fromJNI: selfPointer, in: environment!)))!
pointer.deinitialize(count: 1)
pointer.deallocate()
}
"""
]
)
}
}
Loading