Skip to content
Open
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,7 @@
public class CallbackManager {
private var callback: (() -> Void)?
private var intCallback: ((Int64) -> Int64)?
private var sendableCallback: (@Sendable (Int64) -> Int64)?

public init() {}

Expand All @@ -38,6 +39,14 @@ public class CallbackManager {
public func triggerIntCallback(value: Int64) -> Int64? {
intCallback?(value)
}

public func setSendableCallback(callback: @escaping @Sendable (Int64) -> Int64) {
self.sendableCallback = callback
}

public func triggerSendableCallback(value: Int64) -> Int64? {
sendableCallback?(value)
}
}
// snippet.end

Expand All @@ -64,7 +73,6 @@ public class ClosureStore {
Int64(closures.count)
}
}

public func multipleEscapingClosures(
onSuccess: @escaping (Int64) -> Void,
onFailure: @escaping (Int64) -> Void,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,64 @@ void testCallbackManager_intCallback() {
assertEquals(42, result.getAsLong(), "Callback should double the input");
}
}


@Test
void testCallbackManager_sendableCallback() {
try (var arena = SwiftArena.ofConfined()) {
CallbackManager manager = CallbackManager.init(arena);

// Verify that the generated interface is annotated with @ThreadSafe
assertTrue(
CallbackManager.setSendableCallback.callback.class.isAnnotationPresent(
org.swift.swiftkit.core.annotations.ThreadSafe.class
),
"Sendable callback interface should be annotated with @ThreadSafe"
);

CallbackManager.setSendableCallback.callback callback = (value) -> {
return value * 3;
};

manager.setSendableCallback(callback);

OptionalLong result = manager.triggerSendableCallback(14);
assertTrue(result.isPresent(), "Result should be present");
assertEquals(42, result.getAsLong(), "Sendable callback should multiply input by 3");
}
}

@Test
void testCallbackManager_sendableCallbackConcurrently() throws Exception {
var arena = SwiftArena.ofAuto();
CallbackManager manager = CallbackManager.init(arena);

CallbackManager.setSendableCallback.callback callback = (value) -> {
return value * 10;
};

manager.setSendableCallback(callback);

int threadCount = 4;
var executor = java.util.concurrent.Executors.newFixedThreadPool(threadCount);
var futures = new java.util.ArrayList<java.util.concurrent.Future<Long>>();

for (int i = 0; i < 20; i++) {
final long input = i;
futures.add(executor.submit(() -> {
OptionalLong res = manager.triggerSendableCallback(input);
assertTrue(res.isPresent());
return res.getAsLong();
}));
}

for (int i = 0; i < 20; i++) {
assertEquals(i * 10L, futures.get(i).get());
}

executor.shutdown();
assertTrue(executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS));
}

@Test
void testClosureStore() {
try (var arena = SwiftArena.ofConfined()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ extension FFMSwift2JavaGenerator {
if !isKnownFuncInterface {
// If the user-facing functional interface is C ABI compatible, just extend
// the lowered function pointer parameter interface.
if functionType.swiftType.isSendable {
printer.print("@ThreadSafe // Sendable")
}
printer.print(
"""
@FunctionalInterface
Expand All @@ -336,6 +339,9 @@ extension FFMSwift2JavaGenerator {
"\($0.parameter.type) \($0.parameter.name)"
}

if functionType.swiftType.isSendable {
printer.print("@ThreadSafe // Sendable")
}
printer.print(
"""
@FunctionalInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ extension JNISwift2JavaGenerator {
_ printer: inout SwiftPrinter,
_ closureTy: SyntheticEscapingClosureFunctionType,
) {
let sendableSuffix = closureTy.functionType.isSendable ? ": @unchecked Sendable" : ""
printer.printBraceBlock(
"""
@JavaInterface("\(closureTy.javaBinaryName)")
public struct \(closureTy.javaInterfaceName)
public struct \(closureTy.javaInterfaceName)\(sendableSuffix)
"""
) { p in
let signature = self.renderEscapingClosureApplySignature(closureTy.functionType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,9 @@ extension JNISwift2JavaGenerator {
Corresponds to the Swift closure parameter of type {@code \(functionType.swiftType)}.
"""
)
if functionType.swiftType.isSendable {
printer.print("@ThreadSafe // Sendable")
}
printer.print(
"""
@FunctionalInterface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,8 @@ extension JNISwift2JavaGenerator {
],
conversion: .closureLowering(
parameters: parameters,
result: result
result: result,
functionType: functionType
),
indirectConversion: nil,
conversionCheck: nil
Expand Down Expand Up @@ -1364,7 +1365,7 @@ extension JNISwift2JavaGenerator {
/// of the `Unsafe(Mutable)Pointer` types in Swift.
indirect case pointee(NativeSwiftConversionStep)

indirect case closureLowering(parameters: [NativeParameter], result: NativeResult)
indirect case closureLowering(parameters: [NativeParameter], result: NativeResult, functionType: SwiftFunctionType)

/// Escaping closure lowering using the protocol infrastructure.
/// This uses UpcallConversionStep for full support of optionals, arrays, custom types, etc.
Expand Down Expand Up @@ -1667,7 +1668,7 @@ extension JNISwift2JavaGenerator {
let inner = inner.render(&printer, placeholder)
return "\(inner).pointee"

case .closureLowering(let parameters, let nativeResult):
case .closureLowering(let parameters, let nativeResult, let functionType):
var printer = SwiftPrinter()

let methodSignature = MethodSignature(
Expand All @@ -1683,18 +1684,19 @@ extension JNISwift2JavaGenerator {
)

let names = parameters.flatMap { $0.parameters.map(\.name) }
let closureParameters = !parameters.isEmpty ? "\(names.joined(separator: .comma)) in" : ""
printer.print("{ \(closureParameters)")
let sendablePrefix = functionType.isSendable ? "@Sendable " : ""
let closureParameters = !parameters.isEmpty ? "\(sendablePrefix)\(names.joined(separator: .comma)) in" : (functionType.isSendable ? "@Sendable in" : "")
let space = closureParameters.isEmpty ? "" : " "
printer.print("{\(space)\(closureParameters)")
printer.indent()

// TODO: Add support for types that are lowered to multiple parameters in closures
let arguments = parameters.map {
$0.conversion.render(&printer, $0.parameters.first!.name)
}


let methodName =
if let known = KnownJavaFunctionalInterface.find(methodSignature) {
if let known = KnownJavaFunctionalInterface.find(functionType) {
known.method
} else {
"apply"
Expand Down Expand Up @@ -1755,10 +1757,11 @@ extension JNISwift2JavaGenerator {

// Note: async is part of the closure TYPE, not the closure literal syntax.
// For closures without parameters, we can omit "in" entirely.
let sendablePrefix = fn.isSendable ? "@Sendable " : ""
let closureHeader =
fn.parameters.isEmpty
? "{"
: "{ \(closureParameters) in"
? (fn.isSendable ? "{ @Sendable in" : "{")
: "{ \(sendablePrefix)\(closureParameters) in"

// Construct the generated `@JavaInterface` wrap-java struct.
// It will cause a new global ref on the javaThis, so no need for explicit global refs.
Expand Down
6 changes: 3 additions & 3 deletions Sources/JExtractSwiftLib/KnownFunctionalInterfaces.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ struct KnownJavaFunctionalInterface: Sendable {
}

static func find(_ functionType: SwiftFunctionType) -> KnownJavaFunctionalInterface? {
if functionType.isEscaping {
if functionType.isEscaping || functionType.isSendable {
return nil
}

Expand Down Expand Up @@ -354,14 +354,14 @@ struct KnownJavaFunctionalInterface: Sendable {
}

static func find(_ functionType: JNISwift2JavaGenerator.TranslatedFunctionType) -> KnownJavaFunctionalInterface? {
if functionType.isEscaping {
if functionType.isEscaping || functionType.swiftType.isSendable {
return nil
}
return find(parameters: functionType.parameters, result: functionType.result)
}

static func find(_ functionType: FFMSwift2JavaGenerator.TranslatedFunctionType) -> KnownJavaFunctionalInterface? {
if functionType.swiftType.isEscaping {
if functionType.swiftType.isEscaping || functionType.swiftType.isSendable {
return nil
}
return find(parameters: functionType.parameters.map(\.parameter.type.javaType), result: functionType.result.javaResultType)
Expand Down
8 changes: 7 additions & 1 deletion Sources/SwiftExtract/SwiftTypes/SwiftFunctionType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public struct SwiftFunctionType: Equatable {
public var parameters: [SwiftParameter]
public var resultType: SwiftType
public var isEscaping: Bool = false
public var isSendable: Bool = false

public var effectSpecifiers: [SwiftEffectSpecifier] = []

Expand All @@ -38,13 +39,15 @@ public struct SwiftFunctionType: Equatable {
parameters: [SwiftParameter],
resultType: SwiftType,
isEscaping: Bool = false,
isSendable: Bool = false,
effectSpecifiers: [SwiftEffectSpecifier] = [],
thrownTypedError: SwiftType? = nil
) {
self.convention = convention
self.parameters = parameters
self.resultType = resultType
self.isEscaping = isEscaping
self.isSendable = isSendable
self.effectSpecifiers = effectSpecifiers
self.thrownTypedError = thrownTypedError
}
Expand All @@ -59,14 +62,15 @@ extension SwiftFunctionType: CustomStringConvertible {
case .swift: ""
}
let escapingPrefix = isEscaping ? "@escaping " : ""
let sendablePrefix = isSendable ? "@Sendable " : ""
let throwsString =
switch (isThrowing, thrownTypedError) {
case (true, .some(let errorType)): " throws(\(errorType.description))"
case (true, .none): " throws"
case (false, _): ""
}
let effectsSuffix = (isAsync ? " async" : "") + throwsString
return "\(escapingPrefix)\(conventionPrefix)(\(parameterString))\(effectsSuffix) -> \(resultType.description)"
return "\(escapingPrefix)\(sendablePrefix)\(conventionPrefix)(\(parameterString))\(effectsSuffix) -> \(resultType.description)"
}
}

Expand All @@ -75,10 +79,12 @@ extension SwiftFunctionType {
_ node: FunctionTypeSyntax,
convention: Convention,
isEscaping: Bool = false,
isSendable: Bool = false,
lookupContext: SwiftTypeLookupContext
) throws {
self.convention = convention
self.isEscaping = isEscaping
self.isSendable = isSendable
self.parameters = try node.parameters.map { param in
let isInout = param.inoutKeyword != nil
return SwiftParameter(
Expand Down
6 changes: 6 additions & 0 deletions Sources/SwiftExtract/SwiftTypes/SwiftParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ extension SwiftParameter {

// Determine the type.
self.type = try SwiftType(type, lookupContext: lookupContext)
if node.attributes.trimmedDescription.contains("@Sendable"),
case .function(var fnType) = self.type
{
fnType.isSendable = true
self.type = .function(fnType)
}

// Variadic / default-value information.
self.isVariadic = node.ellipsis != nil
Expand Down
48 changes: 23 additions & 25 deletions Sources/SwiftExtract/SwiftTypes/SwiftType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -379,37 +379,35 @@ extension SwiftType {
self = .inlineArray(count: count, element: elementType)

case .attributedType(let attributedType):
// Recognize "@convention(c)", "@convention(swift)", and "@escaping" attributes on function types.
// Recognize "@convention(c)", "@convention(swift)", "@escaping", and "@Sendable" attributes on function types.
// FIXME: This string matching is a horrible hack.
let attrs = attributedType.attributes.trimmedDescription

// Handle @escaping attribute
if attrs.contains("@escaping") {
let innerType = try SwiftType(attributedType.baseType, lookupContext: lookupContext)
switch innerType {
case .function(var functionType):
let innerType = try SwiftType(attributedType.baseType, lookupContext: lookupContext)
switch innerType {
case .function(var functionType):
var recognized = false
if attrs.contains("@escaping") {
functionType.isEscaping = true
self = .function(functionType)
default:
throw TypeTranslationError.unimplementedType(type)
recognized = true
}
} else {
// Handle @convention attributes
switch attrs {
case "@convention(c)", "@convention(swift)":
let innerType = try SwiftType(attributedType.baseType, lookupContext: lookupContext)
switch innerType {
case .function(var functionType):
let isConventionC = attrs == "@convention(c)"
let convention: SwiftFunctionType.Convention = isConventionC ? .c : .swift
functionType.convention = convention
self = .function(functionType)
default:
throw TypeTranslationError.unimplementedType(type)
}
default:
if attrs.contains("@Sendable") {
functionType.isSendable = true
recognized = true
}
if attrs.contains("@convention(c)") {
functionType.convention = .c
recognized = true
} else if attrs.contains("@convention(swift)") {
functionType.convention = .swift
recognized = true
}
if recognized {
self = .function(functionType)
} else {
throw TypeTranslationError.unimplementedType(type)
}
default:
throw TypeTranslationError.unimplementedType(type)
}

case .functionType(let functionType):
Expand Down
Loading