Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -216,7 +216,9 @@ extension JNISwift2JavaGenerator {
}

// Handle async methods and isolated methods
if decl.functionSignature.isAsync || decl.functionSignature.isIsolated {
if decl.functionSignature.isAsync || decl.functionSignature.isIsolated
|| decl.functionSignature.isImplicitlyAsync
{
self.convertToAsync(
translatedFunctionSignature: &translatedFunctionSignature,
nativeFunctionSignature: &nativeFunctionSignature,
Expand All @@ -231,6 +233,7 @@ extension JNISwift2JavaGenerator {
isThrowing: decl.isThrowing,
isAsync: decl.isAsync,
isIsolated: decl.isIsolated,
isImplicitlyAsync: decl.functionSignature.isImplicitlyAsync,
nativeFunctionName: "$\(javaName)",
parentName: parentName,
functionTypes: funcTypes,
Expand Down Expand Up @@ -1698,6 +1701,8 @@ extension JNISwift2JavaGenerator {

var isIsolated: Bool

var isImplicitlyAsync: Bool

/// The name of the native function
var nativeFunctionName: String

Expand All @@ -1720,7 +1725,7 @@ extension JNISwift2JavaGenerator {

func throwsClause() -> String {
guard !translatedFunctionSignature.exceptions.isEmpty else {
return isThrowing && !(isAsync || isIsolated) ? " throws Exception" : ""
return isThrowing && !(isAsync || isIsolated || isImplicitlyAsync) ? " throws Exception" : ""
}

let signatureExceptions = translatedFunctionSignature.exceptions.compactMap(\.type.className).joined(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@ extension JNISwift2JavaGenerator {
}
}

if decl.isThrowing, !(decl.isAsync || decl.isIsolated) {
if decl.isThrowing, !(decl.isAsync || decl.isIsolated || decl.isImplicitlyAsync) {
printer.print("do {")
printer.indent()
printer.print(innerBody(in: &printer))
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftExtract/ExtractedDecls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ public final class ExtractedFunc: ExtractedSwiftDecl, CustomStringConvertible {
self.functionSignature.isAsync
}

public var isImplicitlyAsync: Bool {
self.functionSignature.isImplicitlyAsync
}

public var isIsolated: Bool {
self.functionSignature.isIsolated
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ public struct SwiftFunctionSignature: Equatable {
effectSpecifiers.contains(.async)
}

public var isImplicitlyAsync: Bool {
guard !isAsync, case .instance(_, let selfType) = selfParameter else {
Comment thread
AbdAlRahmanGad marked this conversation as resolved.
Outdated
return false
}
return selfType.isActor
}

public var isThrowing: Bool {
effectSpecifiers.contains(.throws)
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftExtract/SwiftTypes/SwiftType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public enum SwiftType: Equatable {
asNominalType?.nominalTypeDecl
}

public var isActor: Bool {
asNominalTypeDeclaration?.kind == .actor
}

/// True when this type is a synthetic placeholder produced by SwiftExtract
/// for an unresolved name — see
/// `SwiftNominalTypeDeclaration.isUnresolvedTypePlaceholder` for why these
Expand Down
127 changes: 127 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIActorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import JExtractSwiftLib
import Testing

@Suite
struct JNIActor1MethodsTests {

static let source = """
public actor K {
public init() {}
public func hello() {}
}

extension K {
public func hi() {}
}
"""

@Test("Import: actor method is imported as a future (Java)")
func actorMethod_java() throws {
try assertOutput(
input: Self.source,
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
public java.util.concurrent.CompletableFuture<java.lang.Void> hello() {
java.util.concurrent.CompletableFuture<java.lang.Void> future$ = new java.util.concurrent.CompletableFuture<java.lang.Void>();
K.$hello(this.$memoryAddress(), future$);
return future$.thenApply((futureResult$) -> {
return futureResult$;
}
);
}
""",
"""
private static native void $hello(long selfPointer, java.util.concurrent.CompletableFuture<java.lang.Void> result_future);
""",
],
notExpectedChunks: [
"public void hello() {",
"private static native void $hello(long selfPointer);",
]
)
}

@Test("Import: actor method awaits the actor (Swift)")
func actorMethod_swift() throws {
try assertOutput(
input: Self.source,
.jni,
.swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
@_cdecl("Java_com_example_swift_K__00024hello__JLjava_util_concurrent_CompletableFuture_2")
...
task = Task.immediate {
...
await selfPointer$.pointee.hello()
"""
]
)
}

@Test("Import: actor extension method is imported as a future (Java)")
func actorExtensionMethod_java() throws {
try assertOutput(
input: Self.source,
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
public java.util.concurrent.CompletableFuture<java.lang.Void> hi() {
java.util.concurrent.CompletableFuture<java.lang.Void> future$ = new java.util.concurrent.CompletableFuture<java.lang.Void>();
K.$hi(this.$memoryAddress(), future$);
return future$.thenApply((futureResult$) -> {
return futureResult$;
}
);
}
""",
"""
private static native void $hi(long selfPointer, java.util.concurrent.CompletableFuture<java.lang.Void> result_future);
""",
],
notExpectedChunks: [
"public void hi() {",
"private static native void $hi(long selfPointer);",
]
)
}

@Test("Import: actor extension method awaits the actor (Swift)")
func actorExtensionMethod_swift() throws {
try assertOutput(
input: Self.source,
.jni,
.swift,
detectChunkByInitialLines: 1,
expectedChunks: [
"""
@_cdecl("Java_com_example_swift_K__00024hi__JLjava_util_concurrent_CompletableFuture_2")
...
task = Task.immediate {
...
await selfPointer$.pointee.hi()
"""
]
)
}
}
32 changes: 32 additions & 0 deletions Tests/SwiftExtractTests/AnalysisResultTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -711,4 +711,36 @@ struct AnalysisResultSuite {
let count = try #require(fishTank.variables.first { $0.name == "count" })
#expect(count.isClass)
}

// ==== -----------------------------------------------------------------------
// MARK: Actor members are extracted
@Test func actorMembersAreExtracted() throws {
let result = try analyze(
sources: [
(
"/fake/Source.swift",
"""
public actor K {
public init() {}
public func hello() {}
}

extension K {
public func hi() {}
}
"""
)
],
moduleName: "Aquarium"
)

let k = try #require(result.extractedTypes["K"])
#expect(k.swiftNominal.kind == .actor)

let hello = try #require(k.methods.first { $0.name == "hello" })
#expect(hello.isImplicitlyAsync)

let hi = try #require(k.methods.first { $0.name == "hi" })
#expect(hi.isImplicitlyAsync)
}
}
Loading