Skip to content

Commit b2f42a1

Browse files
authored
[JExtract/JNI] Support optionals in JNI mode (#340)
1 parent 02de967 commit b2f42a1

File tree

17 files changed

+1300
-212
lines changed

17 files changed

+1300
-212
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import JavaKit
16+
17+
public func optionalBool(input: Optional<Bool>) -> Bool? {
18+
return input
19+
}
20+
21+
public func optionalByte(input: Optional<Int8>) -> Int8? {
22+
return input
23+
}
24+
25+
public func optionalChar(input: Optional<UInt16>) -> UInt16? {
26+
return input
27+
}
28+
29+
public func optionalShort(input: Optional<Int16>) -> Int16? {
30+
return input
31+
}
32+
33+
public func optionalInt(input: Optional<Int32>) -> Int32? {
34+
return input
35+
}
36+
37+
public func optionalLong(input: Optional<Int64>) -> Int64? {
38+
return input
39+
}
40+
41+
public func optionalFloat(input: Optional<Float>) -> Float? {
42+
return input
43+
}
44+
45+
public func optionalDouble(input: Optional<Double>) -> Double? {
46+
return input
47+
}
48+
49+
public func optionalString(input: Optional<String>) -> String? {
50+
return input
51+
}
52+
53+
public func optionalClass(input: Optional<MySwiftClass>) -> MySwiftClass? {
54+
return input
55+
}
56+
57+
public func optionalJavaKitLong(input: Optional<JavaLong>) -> Int64? {
58+
if let input {
59+
return input.longValue()
60+
} else {
61+
return nil
62+
}
63+
}
64+
65+
public func multipleOptionals(
66+
input1: Optional<Int8>,
67+
input2: Optional<Int16>,
68+
input3: Optional<Int32>,
69+
input4: Optional<Int64>,
70+
input5: Optional<String>,
71+
input6: Optional<MySwiftClass>,
72+
input7: Optional<Bool>
73+
) -> Int64? {
74+
return 1
75+
}

Samples/JExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java

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

20+
import java.util.Optional;
21+
import java.util.OptionalInt;
22+
import java.util.OptionalLong;
23+
2024
import static org.junit.jupiter.api.Assertions.*;
2125

2226
public class MySwiftClassTest {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
package com.example.swift;
16+
17+
import org.junit.jupiter.api.Test;
18+
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;
19+
20+
import java.util.Optional;
21+
import java.util.OptionalDouble;
22+
import java.util.OptionalInt;
23+
import java.util.OptionalLong;
24+
25+
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
import static org.junit.jupiter.api.Assertions.assertTrue;
27+
28+
public class OptionalsTest {
29+
@Test
30+
void optionalBool() {
31+
assertEquals(Optional.empty(), MySwiftLibrary.optionalBool(Optional.empty()));
32+
assertEquals(Optional.of(true), MySwiftLibrary.optionalBool(Optional.of(true)));
33+
}
34+
35+
@Test
36+
void optionalByte() {
37+
assertEquals(Optional.empty(), MySwiftLibrary.optionalByte(Optional.empty()));
38+
assertEquals(Optional.of((byte) 1) , MySwiftLibrary.optionalByte(Optional.of((byte) 1)));
39+
}
40+
41+
@Test
42+
void optionalChar() {
43+
assertEquals(Optional.empty(), MySwiftLibrary.optionalChar(Optional.empty()));
44+
assertEquals(Optional.of((char) 42), MySwiftLibrary.optionalChar(Optional.of((char) 42)));
45+
}
46+
47+
@Test
48+
void optionalShort() {
49+
assertEquals(Optional.empty(), MySwiftLibrary.optionalShort(Optional.empty()));
50+
assertEquals(Optional.of((short) -250), MySwiftLibrary.optionalShort(Optional.of((short) -250)));
51+
}
52+
53+
@Test
54+
void optionalInt() {
55+
assertEquals(OptionalInt.empty(), MySwiftLibrary.optionalInt(OptionalInt.empty()));
56+
assertEquals(OptionalInt.of(999), MySwiftLibrary.optionalInt(OptionalInt.of(999)));
57+
}
58+
59+
@Test
60+
void optionalLong() {
61+
assertEquals(OptionalLong.empty(), MySwiftLibrary.optionalLong(OptionalLong.empty()));
62+
assertEquals(OptionalLong.of(999), MySwiftLibrary.optionalLong(OptionalLong.of(999)));
63+
}
64+
65+
@Test
66+
void optionalFloat() {
67+
assertEquals(Optional.empty(), MySwiftLibrary.optionalFloat(Optional.empty()));
68+
assertEquals(Optional.of(3.14f), MySwiftLibrary.optionalFloat(Optional.of(3.14f)));
69+
}
70+
71+
@Test
72+
void optionalDouble() {
73+
assertEquals(OptionalDouble.empty(), MySwiftLibrary.optionalDouble(OptionalDouble.empty()));
74+
assertEquals(OptionalDouble.of(2.718), MySwiftLibrary.optionalDouble(OptionalDouble.of(2.718)));
75+
}
76+
77+
@Test
78+
void optionalString() {
79+
assertEquals(Optional.empty(), MySwiftLibrary.optionalString(Optional.empty()));
80+
assertEquals(Optional.of("Hello Swift!"), MySwiftLibrary.optionalString(Optional.of("Hello Swift!")));
81+
}
82+
83+
@Test
84+
void optionalClass() {
85+
try (var arena = new ConfinedSwiftMemorySession()) {
86+
MySwiftClass c = MySwiftClass.init(arena);
87+
assertEquals(Optional.empty(), MySwiftLibrary.optionalClass(Optional.empty(), arena));
88+
Optional<MySwiftClass> optionalClass = MySwiftLibrary.optionalClass(Optional.of(c), arena);
89+
assertTrue(optionalClass.isPresent());
90+
assertEquals(c.getX(), optionalClass.get().getX());
91+
}
92+
}
93+
94+
@Test
95+
void optionalJavaKitLong() {
96+
assertEquals(OptionalLong.empty(), MySwiftLibrary.optionalJavaKitLong(Optional.empty()));
97+
assertEquals(OptionalLong.of(99L), MySwiftLibrary.optionalJavaKitLong(Optional.of(99L)));
98+
}
99+
100+
@Test
101+
void multipleOptionals() {
102+
try (var arena = new ConfinedSwiftMemorySession()) {
103+
MySwiftClass c = MySwiftClass.init(arena);
104+
OptionalLong result = MySwiftLibrary.multipleOptionals(
105+
Optional.of((byte) 1),
106+
Optional.of((short) 42),
107+
OptionalInt.of(50),
108+
OptionalLong.of(1000L),
109+
Optional.of("42"),
110+
Optional.of(c),
111+
Optional.of(true)
112+
);
113+
assertEquals(result, OptionalLong.of(1L));
114+
}
115+
}
116+
}

Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,79 @@ extension JavaType {
3636
case .void: fatalError("There is no type signature for 'void'")
3737
}
3838
}
39+
40+
/// Returns the next integral type with space for self and an additional byte.
41+
var nextIntergralTypeWithSpaceForByte: (javaType: JavaType, swiftType: SwiftKnownTypeDeclKind, valueBytes: Int)? {
42+
switch self {
43+
case .boolean, .byte: (.short, .int16, 1)
44+
case .char, .short: (.int, .int32, 2)
45+
case .int: (.long, .int64, 4)
46+
default: nil
47+
}
48+
}
49+
50+
var optionalType: String? {
51+
switch self {
52+
case .boolean: "Optional<Boolean>"
53+
case .byte: "Optional<Byte>"
54+
case .char: "Optional<Character>"
55+
case .short: "Optional<Short>"
56+
case .int: "OptionalInt"
57+
case .long: "OptionalLong"
58+
case .float: "Optional<Float>"
59+
case .double: "OptionalDouble"
60+
case .javaLangString: "Optional<String>"
61+
default: nil
62+
}
63+
}
64+
65+
var optionalWrapperType: String? {
66+
switch self {
67+
case .boolean, .byte, .char, .short, .float, .javaLangString: "Optional"
68+
case .int: "OptionalInt"
69+
case .long: "OptionalLong"
70+
case .double: "OptionalDouble"
71+
default: nil
72+
}
73+
}
74+
75+
var optionalPlaceholderValue: String? {
76+
switch self {
77+
case .boolean: "false"
78+
case .byte: "(byte) 0"
79+
case .char: "(char) 0"
80+
case .short: "(short) 0"
81+
case .int: "0"
82+
case .long: "0L"
83+
case .float: "0f"
84+
case .double: "0.0"
85+
case .array, .class: "null"
86+
case .void: nil
87+
}
88+
}
89+
90+
var jniCallMethodAName: String {
91+
switch self {
92+
case .boolean: "CallBooleanMethodA"
93+
case .byte: "CallByteMethodA"
94+
case .char: "CallCharMethodA"
95+
case .short: "CallShortMethodA"
96+
case .int: "CallIntMethodA"
97+
case .long: "CallLongMethodA"
98+
case .float: "CallFloatMethodA"
99+
case .double: "CallDoubleMethodA"
100+
case .void: "CallVoidMethodA"
101+
default: "CallObjectMethodA"
102+
}
103+
}
104+
105+
/// Returns whether this type returns `JavaValue` from JavaKit
106+
var implementsJavaValue: Bool {
107+
return switch self {
108+
case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString:
109+
true
110+
default:
111+
false
112+
}
113+
}
39114
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import JavaTypes
1516

1617
// MARK: Defaults
1718

@@ -20,6 +21,7 @@ extension JNISwift2JavaGenerator {
2021
static let defaultJavaImports: Array<String> = [
2122
"org.swift.swiftkit.core.*",
2223
"org.swift.swiftkit.core.util.*",
24+
"java.util.*",
2325

2426
// NonNull, Unsigned and friends
2527
"org.swift.swiftkit.core.annotations.*",
@@ -279,11 +281,15 @@ extension JNISwift2JavaGenerator {
279281
let translatedDecl = translatedDecl(for: decl)! // Will always call with valid decl
280282
let nativeSignature = translatedDecl.nativeFunctionSignature
281283
let resultType = nativeSignature.result.javaType
282-
var parameters = nativeSignature.parameters
283-
if let selfParameter = nativeSignature.selfParameter {
284-
parameters.append(selfParameter)
284+
var parameters = nativeSignature.parameters.flatMap(\.parameters)
285+
if let selfParameter = nativeSignature.selfParameter?.parameters {
286+
parameters += selfParameter
285287
}
286-
let renderedParameters = parameters.map { "\($0.javaType) \($0.name)"}.joined(separator: ", ")
288+
parameters += nativeSignature.result.outParameters
289+
290+
let renderedParameters = parameters.map { javaParameter in
291+
"\(javaParameter.type) \(javaParameter.name)"
292+
}.joined(separator: ", ")
287293

288294
printer.print("private static native \(resultType) \(translatedDecl.nativeFunctionName)(\(renderedParameters));")
289295
}
@@ -308,6 +314,12 @@ extension JNISwift2JavaGenerator {
308314
arguments.append(lowered)
309315
}
310316

317+
// Indirect return receivers
318+
for outParameter in translatedFunctionSignature.resultType.outParameters {
319+
printer.print("\(outParameter.type) \(outParameter.name) = \(outParameter.allocation.render());")
320+
arguments.append(outParameter.name)
321+
}
322+
311323
//=== Part 3: Downcall.
312324
// TODO: If we always generate a native method and a "public" method, we can actually choose our own thunk names
313325
// using the registry?

0 commit comments

Comments
 (0)