Skip to content

[JExtract/JNI] Add support for variables #306

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 6 commits into from
Jul 13, 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 @@ -16,6 +16,31 @@ public class MySwiftClass {
let x: Int64
let y: Int64

public let constant: Int64 = 100
public var mutable: Int64 = 0
public var product: Int64 {
return x * y
}
public var throwingVariable: Int64 {
get throws {
throw MySwiftClassError.swiftError
}
}
public var mutableDividedByTwo: Int64 {
get {
return mutable / 2
}
set {
mutable = newValue * 2
}
}
public let warm: Bool = false
public var getAsync: Int64 {
get async {
return 42
}
}

public static func method() {
p("Hello from static method in a class!")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import Darwin.C
#endif

public var globalVariable: Int64 = 0

public func helloWorld() {
p("\(#function)")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"javaPackage": "com.example.swift",
"mode": "jni"
"mode": "jni",
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ static void examples() {

MySwiftClass.method();

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

System.out.println("myClass.isWarm: " + myClass.isWarm());

try {
myClass.throwingFunction();
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,43 +22,100 @@
public class MySwiftClassTest {
@Test
void init_noParameters() {
try (var arena = new ConfinedSwiftMemorySession(Thread.currentThread())) {
try (var arena = new ConfinedSwiftMemorySession()) {
MySwiftClass c = MySwiftClass.init(arena);
assertNotNull(c);
}
}

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

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

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

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

assertEquals("swiftError", exception.getMessage());
}
}

@Test
void constant() {
try (var arena = new ConfinedSwiftMemorySession()) {
MySwiftClass c = MySwiftClass.init(20, 10, arena);
assertEquals(100, c.getConstant());
}
}

@Test
void mutable() {
try (var arena = new ConfinedSwiftMemorySession()) {
MySwiftClass c = MySwiftClass.init(20, 10, arena);
assertEquals(0, c.getMutable());
c.setMutable(42);
assertEquals(42, c.getMutable());
}
}

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

@Test
void throwingVariable() {
try (var arena = new ConfinedSwiftMemorySession()) {
MySwiftClass c = MySwiftClass.init(20, 10, arena);

Exception exception = assertThrows(Exception.class, () -> c.getThrowingVariable());

assertEquals("swiftError", exception.getMessage());
}
}

@Test
void mutableDividedByTwo() {
try (var arena = new ConfinedSwiftMemorySession()) {
MySwiftClass c = MySwiftClass.init(20, 10, arena);
assertEquals(0, c.getMutableDividedByTwo());
c.setMutable(20);
assertEquals(10, c.getMutableDividedByTwo());
c.setMutableDividedByTwo(5);
assertEquals(10, c.getMutable());
}
}

@Test
void isWarm() {
try (var arena = new ConfinedSwiftMemorySession()) {
MySwiftClass c = MySwiftClass.init(20, 10, arena);
assertFalse(c.isWarm());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,11 @@ void call_writeString_jextract() {

assertEquals(string.length(), reply);
}

@Test
void globalVariable() {
assertEquals(0, MySwiftLibrary.getGlobalVariable());
MySwiftLibrary.setGlobalVariable(100);
assertEquals(100, MySwiftLibrary.getGlobalVariable());
}
}
12 changes: 11 additions & 1 deletion Sources/JExtractSwiftLib/Convenience/String+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,14 @@ extension String {

return "\(f.uppercased())\(String(dropFirst()))"
}
}

/// Returns whether the string is of the format `isX`
var hasJavaBooleanNamingConvention: Bool {
guard self.hasPrefix("is"), self.count > 2 else {
return false
}

let thirdCharacterIndex = self.index(self.startIndex, offsetBy: 2)
return self[thirdCharacterIndex].isUppercase
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ extension FFMSwift2JavaGenerator {

// Name.
let javaName = switch decl.apiKind {
case .getter: "get\(decl.name.toCamelCase)"
case .setter: "set\(decl.name.toCamelCase)"
case .getter: decl.javaGetterName
case .setter: decl.javaSetterName
case .function, .initializer: decl.name
}

Expand Down
18 changes: 18 additions & 0 deletions Sources/JExtractSwiftLib/ImportedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,21 @@ extension ImportedFunc: Hashable {
return lhs === rhs
}
}

extension ImportedFunc {
var javaGetterName: String {
let returnsBoolean = self.functionSignature.result.type.asNominalTypeDeclaration?.knownTypeKind == .bool

if !returnsBoolean {
return "get\(self.name.toCamelCase)"
} else if !self.name.hasJavaBooleanNamingConvention {
return "is\(self.name.toCamelCase)"
} else {
return self.name.toCamelCase
}
}

var javaSetterName: String {
"set\(self.name.toCamelCase)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ extension JNISwift2JavaGenerator {
printFunctionBinding(&printer, decl)
printer.println()
}

for decl in analysis.importedGlobalVariables {
printFunctionBinding(&printer, decl)
printer.println()
}
}
}

Expand Down Expand Up @@ -113,10 +118,17 @@ extension JNISwift2JavaGenerator {

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

for method in decl.methods {
printFunctionBinding(&printer, method)
printer.println()
}

for variable in decl.variables {
printFunctionBinding(&printer, variable)
printer.println()
}

printDestroyFunction(&printer, decl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,15 @@ extension JNISwift2JavaGenerator {
// Types with no parent will be outputted inside a "module" class.
let parentName = decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName

// Name.
let javaName = switch decl.apiKind {
case .getter: decl.javaGetterName
case .setter: decl.javaSetterName
case .function, .initializer: decl.name
}

return TranslatedFunctionDecl(
name: decl.name,
name: javaName,
parentName: parentName,
translatedFunctionSignature: translatedFunctionSignature
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ extension JNISwift2JavaGenerator {
printSwiftFunctionThunk(&printer, decl)
printer.println()
}

for decl in analysis.importedGlobalVariables {
printSwiftFunctionThunk(&printer, decl)
printer.println()
}
}

private func printNominalTypeThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws {
Expand All @@ -102,6 +107,11 @@ extension JNISwift2JavaGenerator {
printer.println()
}

for variable in type.variables {
printSwiftFunctionThunk(&printer, variable)
printer.println()
}

printDestroyFunctionThunk(&printer, type)
}

Expand Down Expand Up @@ -192,19 +202,34 @@ extension JNISwift2JavaGenerator {
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 result: String
switch decl.apiKind {
case .function, .initializer:
let downcallParameters = renderDowncallArguments(
swiftFunctionSignature: decl.functionSignature,
translatedFunctionSignature: translatedDecl.translatedFunctionSignature
)
result = "\(tryClause)\(calleeName).\(decl.name)(\(downcallParameters))"

case .getter:
result = "\(tryClause)\(calleeName).\(decl.name)"

case .setter:
guard let newValueParameter = decl.functionSignature.parameters.first else {
fatalError("Setter did not contain newValue parameter: \(decl)")
}

result = "\(calleeName).\(decl.name) = \(renderJNIToSwiftConversion("newValue", type: newValueParameter.type))"
}

let returnStatement =
if swiftReturnType.isVoid {
functionDowncall
result
} else {
"""
let result = \(functionDowncall)
let result = \(result)
return result.getJNIValue(in: environment)
"""
}
Expand Down Expand Up @@ -320,6 +345,10 @@ extension JNISwift2JavaGenerator {
}
.joined(separator: ", ")
}

private func renderJNIToSwiftConversion(_ variableName: String, type: SwiftType) -> String {
"\(type)(fromJNI: \(variableName), in: environment!)"
}
}

extension String {
Expand Down
11 changes: 7 additions & 4 deletions Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,11 @@ extension SwiftFunctionSignature {
switch binding.accessorBlock?.accessors {
case .getter(let getter):
if let getter = getter.as(AccessorDeclSyntax.self) {
effectSpecifiers = Self.effectSpecifiers(from: getter)
effectSpecifiers = try Self.effectSpecifiers(from: getter)
}
case .accessors(let accessors):
if let getter = accessors.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) {
effectSpecifiers = Self.effectSpecifiers(from: getter)
effectSpecifiers = try Self.effectSpecifiers(from: getter)
}
default:
break
Expand All @@ -232,11 +232,14 @@ extension SwiftFunctionSignature {
}
}

private static func effectSpecifiers(from decl: AccessorDeclSyntax) -> [SwiftEffectSpecifier] {
private static func effectSpecifiers(from decl: AccessorDeclSyntax) throws -> [SwiftEffectSpecifier] {
var effectSpecifiers = [SwiftEffectSpecifier]()
if decl.effectSpecifiers?.throwsClause != nil {
effectSpecifiers.append(.throws)
}
if let asyncSpecifier = decl.effectSpecifiers?.asyncSpecifier {
throw SwiftFunctionTranslationError.async(asyncSpecifier)
}
return effectSpecifiers
}
}
Expand All @@ -254,7 +257,7 @@ extension VariableDeclSyntax {
/// - Parameters:
/// - binding the pattern binding in this declaration.
func supportedAccessorKinds(binding: PatternBindingSyntax) -> SupportedAccessorKinds {
if self.bindingSpecifier == .keyword(.let) {
if self.bindingSpecifier.tokenKind == .keyword(.let) {
return [.get]
}

Expand Down
Loading
Loading