Skip to content

Commit 9af890c

Browse files
authored
jextract/jni: support private(set) by not importing those setters (#809)
Previously we'd assume the setter was there and generate invalid code.
1 parent b673aff commit 9af890c

8 files changed

Lines changed: 225 additions & 4 deletions

File tree

‎Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public class MySwiftClass {
4444
}
4545
}
4646

47+
/// `public private(set)` should expose only a getter to Java; the setter
48+
/// is unreachable so swift-java must not emit a setter thunk for it
49+
public private(set) var privateSetCounter: Int64 = 7
50+
4751
public static func method() {
4852
}
4953

‎Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.junit.jupiter.api.Test;
1818
import org.swift.swiftkit.core.SwiftArena;
1919

20+
import java.lang.reflect.Method;
2021
import java.util.Optional;
2122
import java.util.OptionalInt;
2223
import java.util.OptionalLong;
@@ -187,4 +188,17 @@ void toDebugStringTest() {
187188
assertEquals("debug: MySwiftClass(x: 20, y: 10)", c1.toDebugString());
188189
}
189190
}
191+
192+
@Test
193+
void privateSetCounter_getterOnly() throws Exception {
194+
try (var arena = SwiftArena.ofConfined()) {
195+
MySwiftClass c = MySwiftClass.init(20, 10, arena);
196+
assertEquals(7, c.getPrivateSetCounter());
197+
}
198+
199+
// The setter must not be exposed to Java for `public private(set) var`
200+
Method getter = MySwiftClass.class.getMethod("getPrivateSetCounter");
201+
assertNotNull(getter);
202+
assertThrows(NoSuchMethodException.class, () -> MySwiftClass.class.getMethod("setPrivateSetCounter", long.class));
203+
}
190204
}

‎Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ extension FFMSwift2JavaGenerator {
5959
isSet: Bool,
6060
enclosingType: TypeSyntax? = nil,
6161
) throws -> LoweredFunctionSignature? {
62-
let supportedAccessors = decl.supportedAccessorKinds(binding: decl.bindings.first!)
62+
let supportedAccessors = decl.supportedAccessorKinds(
63+
binding: decl.bindings.first!,
64+
minimumAccessLevel: self.config.effectiveMinimumInputAccessLevelMode,
65+
)
6366
guard supportedAccessors.contains(isSet ? .set : .get) else {
6467
return nil
6568
}

‎Sources/SwiftExtract/Convenience/SwiftSyntax+Extensions.swift‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,27 @@ extension DeclModifierSyntax {
9494
}
9595
}
9696

97+
extension AccessLevelMode {
98+
package func matches(_ modifier: DeclModifierSyntax) -> Bool {
99+
switch (self, modifier.name.tokenKind) {
100+
case (.public, .keyword(.public)),
101+
(.public, .keyword(.open)):
102+
return true
103+
case (.package, .keyword(.public)),
104+
(.package, .keyword(.open)),
105+
(.package, .keyword(.package)):
106+
return true
107+
case (.internal, .keyword(.public)),
108+
(.internal, .keyword(.open)),
109+
(.internal, .keyword(.package)),
110+
(.internal, .keyword(.internal)):
111+
return true
112+
default:
113+
return false
114+
}
115+
}
116+
}
117+
97118
extension WithModifiersSyntax {
98119
package func isPublic(in type: NominalTypeDeclSyntaxNode?) -> Bool {
99120
if let protocolDecl = type?.as(ProtocolDeclSyntax.self) {

‎Sources/SwiftExtract/SwiftAnalysisVisitor.swift‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,10 @@ final class SwiftAnalysisVisitor {
286286
self.log.debug("Import variable: \(node.kind) '\(node.qualifiedNameForDebug)'")
287287

288288
do {
289-
let supportedAccessors = node.supportedAccessorKinds(binding: binding)
289+
let supportedAccessors = node.supportedAccessorKinds(
290+
binding: binding,
291+
minimumAccessLevel: config.effectiveMinimumInputAccessLevelMode,
292+
)
290293
if supportedAccessors.contains(.get) {
291294
try importAccessor(
292295
from: DeclSyntax(node),

‎Sources/SwiftExtract/SwiftTypes/SwiftFunctionSignature.swift‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,12 @@ extension VariableDeclSyntax {
458458
/// Determine what operations (i.e. get and/or set) supported in this `VariableDeclSyntax`
459459
///
460460
/// - Parameters:
461-
/// - binding the pattern binding in this declaration.
462-
public func supportedAccessorKinds(binding: PatternBindingSyntax) -> AccessorBlockSyntax.SupportedAccessorKinds {
461+
/// - binding: the pattern binding in this declaration.
462+
/// - minimumAccessLevel: the minimum access level being extracted
463+
public func supportedAccessorKinds(
464+
binding: PatternBindingSyntax,
465+
minimumAccessLevel: AccessLevelMode
466+
) -> AccessorBlockSyntax.SupportedAccessorKinds {
463467
if self.bindingSpecifier.tokenKind == .keyword(.let) {
464468
return [.get]
465469
}
@@ -468,6 +472,13 @@ extension VariableDeclSyntax {
468472
return accessorBlock.supportedAccessorKinds()
469473
}
470474

475+
// Account for private(set) and similar modifiers
476+
for modifier in self.modifiers where modifier.detail?.detail.text == "set" {
477+
if !minimumAccessLevel.matches(modifier) {
478+
return [.get]
479+
}
480+
}
481+
471482
return [.get, .set]
472483
}
473484
}

‎Tests/JExtractSwiftTests/JNI/JNIVariablesTests.swift‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import JExtractSwiftLib
16+
import SwiftJavaConfigurationShared
1617
import Testing
1718

1819
@Suite
@@ -35,6 +36,8 @@ struct JNIVariablesTests {
3536
}
3637
public var someBoolean: Bool
3738
public var isBoolean: Bool
39+
public private(set) var privateSetCounter: Int64
40+
public internal(set) var internalSetCounter: Int64
3841
}
3942
"""
4043

@@ -447,4 +450,135 @@ struct JNIVariablesTests {
447450
]
448451
)
449452
}
453+
454+
@Test
455+
func privateSetCounter_javaBindings() throws {
456+
try assertOutput(
457+
input: membersSource,
458+
.jni,
459+
.java,
460+
detectChunkByInitialLines: 8,
461+
expectedChunks: [
462+
"""
463+
/**
464+
* Downcall to Swift:
465+
* {@snippet lang=swift :
466+
* public private(set) var privateSetCounter: Int64
467+
* }
468+
*/
469+
public long getPrivateSetCounter() {
470+
return MyClass.$getPrivateSetCounter(this.$memoryAddress());
471+
}
472+
""",
473+
"""
474+
private static native long $getPrivateSetCounter(long selfPointer);
475+
""",
476+
],
477+
notExpectedChunks: [
478+
"setPrivateSetCounter",
479+
"$setPrivateSetCounter",
480+
]
481+
)
482+
}
483+
484+
@Test
485+
func privateSetCounter_swiftThunks() throws {
486+
try assertOutput(
487+
input: membersSource,
488+
.jni,
489+
.swift,
490+
detectChunkByInitialLines: 1,
491+
expectedChunks: [
492+
"""
493+
@_cdecl("Java_com_example_swift_MyClass__00024getPrivateSetCounter__J")
494+
public func Java_com_example_swift_MyClass__00024getPrivateSetCounter__J(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, selfPointer: jlong) -> jlong {
495+
...
496+
return selfPointer$.pointee.privateSetCounter.getJNILocalRefValue(in: environment)
497+
}
498+
"""
499+
],
500+
notExpectedChunks: [
501+
"setPrivateSetCounter"
502+
]
503+
)
504+
}
505+
506+
@Test
507+
func internalSetCounter_javaBindings() throws {
508+
try assertOutput(
509+
input: membersSource,
510+
.jni,
511+
.java,
512+
detectChunkByInitialLines: 8,
513+
expectedChunks: [
514+
"""
515+
/**
516+
* Downcall to Swift:
517+
* {@snippet lang=swift :
518+
* public internal(set) var internalSetCounter: Int64
519+
* }
520+
*/
521+
public long getInternalSetCounter() {
522+
return MyClass.$getInternalSetCounter(this.$memoryAddress());
523+
}
524+
""",
525+
"""
526+
private static native long $getInternalSetCounter(long selfPointer);
527+
""",
528+
],
529+
notExpectedChunks: [
530+
"setInternalSetCounter",
531+
"$setInternalSetCounter",
532+
]
533+
)
534+
}
535+
536+
@Test
537+
func packageSet_javaBindings_atPackageLevel() throws {
538+
let src =
539+
"""
540+
public class MyClass {
541+
public package(set) var packageSetCounter: Int64
542+
}
543+
"""
544+
var config = Configuration()
545+
config.minimumInputAccessLevelMode = .package
546+
try assertOutput(
547+
input: src,
548+
config: config,
549+
.jni,
550+
.java,
551+
detectChunkByInitialLines: 8,
552+
expectedChunks: [
553+
"""
554+
/**
555+
* Downcall to Swift:
556+
* {@snippet lang=swift :
557+
* public package(set) var packageSetCounter: Int64
558+
* }
559+
*/
560+
public long getPackageSetCounter() {
561+
return MyClass.$getPackageSetCounter(this.$memoryAddress());
562+
}
563+
""",
564+
"""
565+
/**
566+
* Downcall to Swift:
567+
* {@snippet lang=swift :
568+
* public package(set) var packageSetCounter: Int64
569+
* }
570+
*/
571+
public void setPackageSetCounter(long newValue) {
572+
MyClass.$setPackageSetCounter(newValue, this.$memoryAddress());
573+
}
574+
""",
575+
"""
576+
private static native long $getPackageSetCounter(long selfPointer);
577+
""",
578+
"""
579+
private static native void $setPackageSetCounter(long newValue, long selfPointer);
580+
""",
581+
]
582+
)
583+
}
450584
}

‎Tests/JExtractSwiftTests/VariableImportTests.swift‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,35 @@ final class VariableImportTests {
123123
]
124124
)
125125
}
126+
127+
let class_privateSetInterfaceFile =
128+
"""
129+
public class MySwiftClass {
130+
public private(set) var counterInt: Int
131+
}
132+
"""
133+
134+
@Test("Import: public private(set) var counterInt: Int emits only the getter")
135+
func variable_int_privateSet() throws {
136+
try assertOutput(
137+
input: class_privateSetInterfaceFile,
138+
.ffm,
139+
.java,
140+
swiftModuleName: "FakeModule",
141+
detectChunkByInitialLines: 1,
142+
expectedChunks: [
143+
"""
144+
public long getCounterInt() throws SwiftIntegerOverflowException {
145+
$ensureAlive();
146+
long result$checked = swiftjava_FakeModule_MySwiftClass_counterInt$get.call(this.$memorySegment());
147+
...
148+
}
149+
"""
150+
],
151+
notExpectedChunks: [
152+
"swiftjava_FakeModule_MySwiftClass_counterInt$set",
153+
"setCounterInt",
154+
]
155+
)
156+
}
126157
}

0 commit comments

Comments
 (0)