Skip to content

Commit 19a6785

Browse files
authored
[JExtract/FFM] Bridge Optional parameters (#320)
1 parent 78b3e20 commit 19a6785

File tree

14 files changed

+449
-19
lines changed

14 files changed

+449
-19
lines changed

Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ public func globalReceiveSomeDataProtocol(data: some DataProtocol) -> Int {
6868
return data.count
6969
}
7070

71+
public func globalReceiveOptional(o1: Int?, o2: (some DataProtocol)?) -> Int {
72+
switch (o1, o2) {
73+
case (nil, nil):
74+
p("<nil>, <nil>")
75+
return 0
76+
case (let v1?, nil):
77+
p("\(v1), <nil>")
78+
return 1
79+
case (nil, let v2?):
80+
p("<nil>, \(v2)")
81+
return 2
82+
case (let v1?, let v2?):
83+
p("\(v1), \(v2)")
84+
return 3
85+
}
86+
}
87+
7188
// ==== Internal helpers
7289

7390
func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) {

Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import org.swift.swiftkit.ffm.AllocatingSwiftArena;
2323
import org.swift.swiftkit.ffm.SwiftRuntime;
2424

25+
import java.util.Optional;
26+
import java.util.OptionalLong;
27+
2528
public class HelloJava2Swift {
2629

2730
public static void main(String[] args) {
@@ -95,6 +98,7 @@ static void examples() {
9598
var bytes = arena.allocateFrom("hello");
9699
var dat = Data.init(bytes, bytes.byteSize(), arena);
97100
MySwiftLibrary.globalReceiveSomeDataProtocol(dat);
101+
MySwiftLibrary.globalReceiveOptional(OptionalLong.of(12), Optional.of(dat));
98102
}
99103

100104

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.ffm.AllocatingSwiftArena;
19+
20+
import java.util.Optional;
21+
import java.util.OptionalLong;
22+
23+
import static org.junit.jupiter.api.Assertions.*;
24+
25+
public class OptionalImportTest {
26+
@Test
27+
void test_Optional_receive() {
28+
try (var arena = AllocatingSwiftArena.ofConfined()) {
29+
var origBytes = arena.allocateFrom("foobar");
30+
var data = Data.init(origBytes, origBytes.byteSize(), arena);
31+
assertEquals(0, MySwiftLibrary.globalReceiveOptional(OptionalLong.empty(), Optional.empty()));
32+
assertEquals(3, MySwiftLibrary.globalReceiveOptional(OptionalLong.of(12), Optional.of(data)));
33+
}
34+
}
35+
}

Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ extension SwiftKnownTypeDeclKind {
125125
.qualified(const: true, volatile: false, type: .void)
126126
)
127127
case .void: .void
128-
case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .data, .dataProtocol:
128+
case .unsafePointer, .unsafeMutablePointer, .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer, .unsafeBufferPointer, .unsafeMutableBufferPointer, .string, .data, .dataProtocol, .optional:
129129
nil
130130
}
131131
}

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

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ struct CdeclLowering {
194194
)
195195

196196
case .unsafeBufferPointer, .unsafeMutableBufferPointer:
197-
guard let genericArgs = type.asNominalType?.genericArguments, genericArgs.count == 1 else {
197+
guard let genericArgs = nominal.genericArguments, genericArgs.count == 1 else {
198198
throw LoweringError.unhandledType(type)
199199
}
200200
// Typed pointers are lowered to (raw-pointer, count) pair.
@@ -253,6 +253,12 @@ struct CdeclLowering {
253253
]
254254
))
255255

256+
case .optional:
257+
guard let genericArgs = nominal.genericArguments, genericArgs.count == 1 else {
258+
throw LoweringError.unhandledType(type)
259+
}
260+
return try lowerOptionalParameter(genericArgs[0], convention: convention, parameterName: parameterName)
261+
256262
case .string:
257263
// 'String' is passed in by C string. i.e. 'UnsafePointer<Int8>' ('const uint8_t *')
258264
if knownType == .string {
@@ -336,8 +342,79 @@ struct CdeclLowering {
336342
}
337343
throw LoweringError.unhandledType(type)
338344

339-
case .optional:
340-
throw LoweringError.unhandledType(type)
345+
case .optional(let wrapped):
346+
return try lowerOptionalParameter(wrapped, convention: convention, parameterName: parameterName)
347+
}
348+
}
349+
350+
/// Lower a Swift Optional to cdecl function type.
351+
///
352+
/// - Parameters:
353+
/// - fn: the Swift function type to lower.
354+
func lowerOptionalParameter(
355+
_ wrappedType: SwiftType,
356+
convention: SwiftParameterConvention,
357+
parameterName: String
358+
) throws -> LoweredParameter {
359+
// If there is a 1:1 mapping between this Swift type and a C type, lower it to 'UnsafePointer<T>?'
360+
if let _ = try? CType(cdeclType: wrappedType) {
361+
return LoweredParameter(
362+
cdeclParameters: [
363+
SwiftParameter(convention: .byValue, parameterName: parameterName, type: .optional(knownTypes.unsafePointer(wrappedType)))
364+
],
365+
conversion: .pointee(.optionalChain(.placeholder))
366+
)
367+
}
368+
369+
switch wrappedType {
370+
case .nominal(let nominal):
371+
if let knownType = nominal.nominalTypeDecl.knownTypeKind {
372+
switch knownType {
373+
case .data:
374+
break
375+
case .unsafeRawPointer, .unsafeMutableRawPointer:
376+
throw LoweringError.unhandledType(.optional(wrappedType))
377+
case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer:
378+
throw LoweringError.unhandledType(.optional(wrappedType))
379+
case .unsafePointer, .unsafeMutablePointer:
380+
throw LoweringError.unhandledType(.optional(wrappedType))
381+
case .unsafeBufferPointer, .unsafeMutableBufferPointer:
382+
throw LoweringError.unhandledType(.optional(wrappedType))
383+
case .void, .string:
384+
throw LoweringError.unhandledType(.optional(wrappedType))
385+
case .dataProtocol:
386+
throw LoweringError.unhandledType(.optional(wrappedType))
387+
default:
388+
// Unreachable? Should be handled by `CType(cdeclType:)` lowering above.
389+
throw LoweringError.unhandledType(.optional(wrappedType))
390+
}
391+
}
392+
393+
// Lower arbitrary nominal to `UnsafeRawPointer?`
394+
return LoweredParameter(
395+
cdeclParameters: [
396+
SwiftParameter(convention: .byValue, parameterName: parameterName, type: .optional(knownTypes.unsafeRawPointer))
397+
],
398+
conversion: .pointee(.typedPointer(.optionalChain(.placeholder), swiftType: wrappedType))
399+
)
400+
401+
case .existential(let proto), .opaque(let proto):
402+
if
403+
let knownProtocol = proto.asNominalTypeDeclaration?.knownTypeKind,
404+
let concreteTy = knownTypes.representativeType(of: knownProtocol)
405+
{
406+
return try lowerOptionalParameter(concreteTy, convention: convention, parameterName: parameterName)
407+
}
408+
throw LoweringError.unhandledType(.optional(wrappedType))
409+
410+
case .tuple(let tuple):
411+
if tuple.count == 1 {
412+
return try lowerOptionalParameter(tuple[0], convention: convention, parameterName: parameterName)
413+
}
414+
throw LoweringError.unhandledType(.optional(wrappedType))
415+
416+
case .function, .metatype, .optional:
417+
throw LoweringError.unhandledType(.optional(wrappedType))
341418
}
342419
}
343420

@@ -527,13 +604,13 @@ struct CdeclLowering {
527604
case .void:
528605
return LoweredResult(cdeclResultType: .void, cdeclOutParameters: [], conversion: .placeholder)
529606

530-
case .string:
531-
// Returning string is not supported at this point.
532-
throw LoweringError.unhandledType(type)
533-
534607
case .data:
535608
break
536609

610+
case .string, .optional:
611+
// Not supported at this point.
612+
throw LoweringError.unhandledType(type)
613+
537614
default:
538615
// Unreachable? Should be handled by `CType(cdeclType:)` lowering above.
539616
throw LoweringError.unhandledType(type)

Sources/JExtractSwiftLib/FFM/ConversionStep.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ enum ConversionStep: Equatable {
6262

6363
indirect case member(ConversionStep, member: String)
6464

65+
indirect case optionalChain(ConversionStep)
66+
6567
/// Count the number of times that the placeholder occurs within this
6668
/// conversion step.
6769
var placeholderCount: Int {
@@ -71,7 +73,7 @@ enum ConversionStep: Equatable {
7173
.typedPointer(let inner, swiftType: _),
7274
.unsafeCastPointer(let inner, swiftType: _),
7375
.populatePointer(name: _, assumingType: _, to: let inner),
74-
.member(let inner, member: _):
76+
.member(let inner, member: _), .optionalChain(let inner):
7577
inner.placeholderCount
7678
case .initialize(_, arguments: let arguments):
7779
arguments.reduce(0) { $0 + $1.argument.placeholderCount }
@@ -175,6 +177,10 @@ enum ConversionStep: Equatable {
175177
}
176178
return nil
177179

180+
case .optionalChain(let step):
181+
let inner = step.asExprSyntax(placeholder: placeholder, bodyItems: &bodyItems)
182+
return ExprSyntax(OptionalChainingExprSyntax(expression: inner!))
183+
178184
case .closureLowering(let parameterSteps, let resultStep):
179185
var body: [CodeBlockItemSyntax] = []
180186

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ extension FFMSwift2JavaGenerator {
238238
throw JavaTranslationError.unhandledType(type)
239239
}
240240

241-
242241
/// Translate a Swift API signature to the user-facing Java API signature.
243242
///
244243
/// Note that the result signature is for the high-level Java API, not the
@@ -349,6 +348,12 @@ extension FFMSwift2JavaGenerator {
349348
])
350349
)
351350

351+
case .optional:
352+
guard let genericArgs = swiftNominalType.genericArguments, genericArgs.count == 1 else {
353+
throw JavaTranslationError.unhandledType(swiftType)
354+
}
355+
return try translateOptionalParameter(wrappedType: genericArgs[0], convention: convention, parameterName: parameterName, loweredParam: loweredParam, methodName: methodName)
356+
352357
case .string:
353358
return TranslatedParameter(
354359
javaParameters: [
@@ -414,8 +419,81 @@ extension FFMSwift2JavaGenerator {
414419
// Otherwise, not supported yet.
415420
throw JavaTranslationError.unhandledType(swiftType)
416421

417-
case .optional:
418-
throw JavaTranslationError.unhandledType(swiftType)
422+
case .optional(let wrapped):
423+
return try translateOptionalParameter(wrappedType: wrapped, convention: convention, parameterName: parameterName, loweredParam: loweredParam, methodName: methodName)
424+
}
425+
}
426+
427+
/// Translate an Optional Swift API parameter to the user-facing Java API parameter.
428+
func translateOptionalParameter(
429+
wrappedType swiftType: SwiftType,
430+
convention: SwiftParameterConvention,
431+
parameterName: String,
432+
loweredParam: LoweredParameter,
433+
methodName: String
434+
) throws -> TranslatedParameter {
435+
// If there is a 1:1 mapping between this Swift type and a C type, that can
436+
// be expressed as a Java primitive type.
437+
if let cType = try? CType(cdeclType: swiftType) {
438+
var (translatedClass, lowerFunc) = switch cType.javaType {
439+
case .int: ("OptionalInt", "toOptionalSegmentInt")
440+
case .long: ("OptionalLong", "toOptionalSegmentLong")
441+
case .double: ("OptionalDouble", "toOptionalSegmentDouble")
442+
case .boolean: ("Optional<Boolean>", "toOptionalSegmentBoolean")
443+
case .byte: ("Optional<Byte>", "toOptionalSegmentByte")
444+
case .char: ("Optional<Character>", "toOptionalSegmentCharacter")
445+
case .short: ("Optional<Short>", "toOptionalSegmentShort")
446+
case .float: ("Optional<Float>", "toOptionalSegmentFloat")
447+
default:
448+
throw JavaTranslationError.unhandledType(.optional(swiftType))
449+
}
450+
return TranslatedParameter(
451+
javaParameters: [
452+
JavaParameter(name: parameterName, type: JavaType(className: translatedClass))
453+
],
454+
conversion: .call(.placeholder, function: "SwiftRuntime.\(lowerFunc)", withArena: true)
455+
)
456+
}
457+
458+
switch swiftType {
459+
case .nominal(let nominal):
460+
if let knownType = nominal.nominalTypeDecl.knownTypeKind {
461+
switch knownType {
462+
case .data, .dataProtocol:
463+
break
464+
default:
465+
throw JavaTranslationError.unhandledType(.optional(swiftType))
466+
}
467+
}
468+
469+
let translatedTy = try self.translate(swiftType: swiftType)
470+
return TranslatedParameter(
471+
javaParameters: [
472+
JavaParameter(name: parameterName, type: JavaType(className: "Optional<\(translatedTy.description)>"))
473+
],
474+
conversion: .call(.placeholder, function: "SwiftRuntime.toOptionalSegmentInstance", withArena: false)
475+
)
476+
case .existential(let proto), .opaque(let proto):
477+
if
478+
let knownProtocol = proto.asNominalTypeDeclaration?.knownTypeKind,
479+
let concreteTy = knownTypes.representativeType(of: knownProtocol)
480+
{
481+
return try translateOptionalParameter(
482+
wrappedType: concreteTy,
483+
convention: convention,
484+
parameterName: parameterName,
485+
loweredParam: loweredParam,
486+
methodName: methodName
487+
)
488+
}
489+
throw JavaTranslationError.unhandledType(.optional(swiftType))
490+
case .tuple(let tuple):
491+
if tuple.count == 1 {
492+
return try translateOptionalParameter(wrappedType: tuple[0], convention: convention, parameterName: parameterName, loweredParam: loweredParam, methodName: methodName)
493+
}
494+
throw JavaTranslationError.unhandledType(.optional(swiftType))
495+
default:
496+
throw JavaTranslationError.unhandledType(.optional(swiftType))
419497
}
420498
}
421499

@@ -585,7 +663,6 @@ extension FFMSwift2JavaGenerator.TranslatedFunctionSignature {
585663
}
586664
}
587665

588-
589666
extension CType {
590667
/// Map lowered C type to Java type for FFM binding.
591668
var javaType: JavaType {

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,7 @@ extension FFMSwift2JavaGenerator {
101101
// Necessary for native calls and type mapping
102102
"java.lang.foreign.*",
103103
"java.lang.invoke.*",
104-
"java.util.Arrays",
105-
"java.util.stream.Collectors",
106-
"java.util.concurrent.atomic.*",
104+
"java.util.*",
107105
"java.nio.charset.StandardCharsets",
108106
]
109107
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ extension JNISwift2JavaGenerator {
112112
.unsafeRawPointer, .unsafeMutableRawPointer,
113113
.unsafePointer, .unsafeMutablePointer,
114114
.unsafeRawBufferPointer, .unsafeMutableRawBufferPointer,
115-
.unsafeBufferPointer, .unsafeMutableBufferPointer, .data, .dataProtocol:
115+
.unsafeBufferPointer, .unsafeMutableBufferPointer, .optional, .data, .dataProtocol:
116116
nil
117117
}
118118
}

Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ private let swiftSourceFile: SourceFileSyntax = """
7676
public struct UnsafeBufferPointer<Element> {}
7777
public struct UnsafeMutableBufferPointer<Element> {}
7878
79+
public struct Optional<Wrapped> {}
80+
7981
// FIXME: Support 'typealias Void = ()'
8082
public struct Void {}
8183

0 commit comments

Comments
 (0)