Skip to content

Commit 5c69766

Browse files
authored
[jextract] add support for throwing functions in JNI mode (#277)
1 parent d0eeacc commit 5c69766

File tree

6 files changed

+163
-22
lines changed

6 files changed

+163
-22
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,8 @@ public struct LoweredFunctionSignature: Equatable {
526526
SwiftFunctionSignature(
527527
selfParameter: nil,
528528
parameters: allLoweredParameters,
529-
result: SwiftResult(convention: .direct, type: result.cdeclResultType)
529+
result: SwiftResult(convention: .direct, type: result.cdeclResultType),
530+
effectSpecifiers: []
530531
)
531532
}
532533
}

Sources/JExtractSwiftLib/ImportedDecls.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ public final class ImportedFunc: ImportedDecl, CustomStringConvertible {
9898
return prefix + context + self.name
9999
}
100100

101+
var isThrowing: Bool {
102+
self.functionSignature.effectSpecifiers.contains(.throws)
103+
}
104+
101105
init(
102106
module: String,
103107
swiftDecl: any DeclSyntaxProtocol,

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ extension JNISwift2JavaGenerator {
124124
"thisClass: jclass"
125125
] + translatedParameters.map { "\($0.0): \($0.1.jniTypeName)"}
126126
let swiftReturnType = decl.functionSignature.result.type
127-
128127
let thunkReturnType = !swiftReturnType.isVoid ? " -> \(swiftReturnType.javaType.jniTypeName)" : ""
129128

130129
printer.printBraceBlock(
@@ -137,17 +136,32 @@ extension JNISwift2JavaGenerator {
137136
let label = originalParam.argumentLabel.map { "\($0): "} ?? ""
138137
return "\(label)\(originalParam.type)(fromJNI: \(translatedParam.0), in: environment!)"
139138
}
140-
let functionDowncall = "\(swiftModuleName).\(decl.name)(\(downcallParameters.joined(separator: ", ")))"
139+
let tryClause: String = decl.isThrowing ? "try " : ""
140+
let functionDowncall = "\(tryClause)\(swiftModuleName).\(decl.name)(\(downcallParameters.joined(separator: ", ")))"
141141

142-
if swiftReturnType.isVoid {
143-
printer.print(functionDowncall)
142+
let innerBody = if swiftReturnType.isVoid {
143+
functionDowncall
144144
} else {
145+
"""
146+
let result = \(functionDowncall)
147+
return result.getJNIValue(in: environment)")
148+
"""
149+
}
150+
151+
if decl.isThrowing {
152+
let dummyReturn = !swiftReturnType.isVoid ? "return \(swiftReturnType).jniPlaceholderValue" : ""
145153
printer.print(
146154
"""
147-
let result = \(functionDowncall)
148-
return result.getJNIValue(in: environment)")
155+
do {
156+
\(innerBody)
157+
} catch {
158+
environment.throwAsException(error)
159+
\(dummyReturn)
160+
}
149161
"""
150162
)
163+
} else {
164+
printer.print(innerBody)
151165
}
152166
}
153167
}
@@ -197,6 +211,7 @@ extension JNISwift2JavaGenerator {
197211
let params = decl.functionSignature.parameters.enumerated().map { idx, param in
198212
"\(param.type.javaType) \(param.parameterName ?? "arg\(idx))")"
199213
}
214+
let throwsClause = decl.isThrowing ? " throws Exception" : ""
200215

201216
printer.print(
202217
"""
@@ -208,7 +223,7 @@ extension JNISwift2JavaGenerator {
208223
*/
209224
"""
210225
)
211-
printer.print("public static native \(returnType) \(decl.name)(\(params.joined(separator: ", ")));")
226+
printer.print("public static native \(returnType) \(decl.name)(\(params.joined(separator: ", ")))\(throwsClause);")
212227
}
213228
}
214229

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
enum SwiftEffectSpecifier: Equatable {
16+
case `throws`
17+
}

Sources/JExtractSwiftLib/SwiftTypes/SwiftFunctionSignature.swift

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,18 @@ public struct SwiftFunctionSignature: Equatable {
2121
var selfParameter: SwiftSelfParameter?
2222
var parameters: [SwiftParameter]
2323
var result: SwiftResult
24+
var effectSpecifiers: [SwiftEffectSpecifier]
2425

25-
init(selfParameter: SwiftSelfParameter? = nil, parameters: [SwiftParameter], result: SwiftResult) {
26+
init(
27+
selfParameter: SwiftSelfParameter? = nil,
28+
parameters: [SwiftParameter],
29+
result: SwiftResult,
30+
effectSpecifiers: [SwiftEffectSpecifier]
31+
) {
2632
self.selfParameter = selfParameter
2733
self.parameters = parameters
2834
self.result = result
35+
self.effectSpecifiers = effectSpecifiers
2936
}
3037
}
3138

@@ -68,13 +75,16 @@ extension SwiftFunctionSignature {
6875
throw SwiftFunctionTranslationError.generic(generics)
6976
}
7077

78+
let (parameters, effectSpecifiers) = try Self.translateFunctionSignature(
79+
node.signature,
80+
symbolTable: symbolTable
81+
)
82+
7183
self.init(
7284
selfParameter: .initializer(enclosingType),
73-
parameters: try Self.translateFunctionSignature(
74-
node.signature,
75-
symbolTable: symbolTable
76-
),
77-
result: SwiftResult(convention: .direct, type: enclosingType)
85+
parameters: parameters,
86+
result: SwiftResult(convention: .direct, type: enclosingType),
87+
effectSpecifiers: effectSpecifiers
7888
)
7989
}
8090

@@ -120,7 +130,7 @@ extension SwiftFunctionSignature {
120130
}
121131

122132
// Translate the parameters.
123-
let parameters = try Self.translateFunctionSignature(
133+
let (parameters, effectSpecifiers) = try Self.translateFunctionSignature(
124134
node.signature,
125135
symbolTable: symbolTable
126136
)
@@ -136,26 +146,28 @@ extension SwiftFunctionSignature {
136146
result = .void
137147
}
138148

139-
self.init(selfParameter: selfParameter, parameters: parameters, result: result)
149+
self.init(selfParameter: selfParameter, parameters: parameters, result: result, effectSpecifiers: effectSpecifiers)
140150
}
141151

142152
/// Translate the function signature, returning the list of translated
143-
/// parameters.
153+
/// parameters and effect specifiers.
144154
static func translateFunctionSignature(
145155
_ signature: FunctionSignatureSyntax,
146156
symbolTable: SwiftSymbolTable
147-
) throws -> [SwiftParameter] {
148-
// FIXME: Prohibit effects for now.
149-
if let throwsClause = signature.effectSpecifiers?.throwsClause {
150-
throw SwiftFunctionTranslationError.throws(throwsClause)
157+
) throws -> ([SwiftParameter], [SwiftEffectSpecifier]) {
158+
var effectSpecifiers = [SwiftEffectSpecifier]()
159+
if signature.effectSpecifiers?.throwsClause != nil {
160+
effectSpecifiers.append(.throws)
151161
}
152162
if let asyncSpecifier = signature.effectSpecifiers?.asyncSpecifier {
153163
throw SwiftFunctionTranslationError.async(asyncSpecifier)
154164
}
155165

156-
return try signature.parameterClause.parameters.map { param in
166+
let parameters = try signature.parameterClause.parameters.map { param in
157167
try SwiftParameter(param, symbolTable: symbolTable)
158168
}
169+
170+
return (parameters, effectSpecifiers)
159171
}
160172

161173
init(_ varNode: VariableDeclSyntax, isSet: Bool, enclosingType: SwiftType?, symbolTable: SwiftSymbolTable) throws {
@@ -195,6 +207,22 @@ extension SwiftFunctionSignature {
195207
}
196208
let valueType = try SwiftType(varTypeNode, symbolTable: symbolTable)
197209

210+
var effectSpecifiers: [SwiftEffectSpecifier]? = nil
211+
switch binding.accessorBlock?.accessors {
212+
case .getter(let getter):
213+
if let getter = getter.as(AccessorDeclSyntax.self) {
214+
effectSpecifiers = Self.effectSpecifiers(from: getter)
215+
}
216+
case .accessors(let accessors):
217+
if let getter = accessors.first(where: { $0.accessorSpecifier.tokenKind == .keyword(.get) }) {
218+
effectSpecifiers = Self.effectSpecifiers(from: getter)
219+
}
220+
default:
221+
break
222+
}
223+
224+
self.effectSpecifiers = effectSpecifiers ?? []
225+
198226
if isSet {
199227
self.parameters = [SwiftParameter(convention: .byValue, parameterName: "newValue", type: valueType)]
200228
self.result = .void
@@ -203,6 +231,14 @@ extension SwiftFunctionSignature {
203231
self.result = .init(convention: .direct, type: valueType)
204232
}
205233
}
234+
235+
private static func effectSpecifiers(from decl: AccessorDeclSyntax) -> [SwiftEffectSpecifier] {
236+
var effectSpecifiers = [SwiftEffectSpecifier]()
237+
if decl.effectSpecifiers?.throwsClause != nil {
238+
effectSpecifiers.append(.throws)
239+
}
240+
return effectSpecifiers
241+
}
206242
}
207243

208244
extension VariableDeclSyntax {

Tests/JExtractSwiftTests/JNI/JNIModuleTests.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ struct JNIModuleTests {
2727
public func copy(_ string: String) -> String
2828
"""
2929

30+
let globalMethodThrowing = """
31+
public func methodA() throws
32+
public func methodB() throws -> Int64
33+
"""
34+
3035
@Test
3136
func generatesModuleJavaClass() throws {
3237
let input = "public func helloWorld()"
@@ -150,4 +155,67 @@ struct JNIModuleTests {
150155
]
151156
)
152157
}
158+
159+
@Test
160+
func globalMethodThrowing_javaBindings() throws {
161+
try assertOutput(
162+
input: globalMethodThrowing,
163+
.jni,
164+
.java,
165+
expectedChunks: [
166+
"""
167+
/**
168+
* Downcall to Swift:
169+
* {@snippet lang=swift :
170+
* public func methodA() throws
171+
* }
172+
*/
173+
public static native void methodA() throws Exception;
174+
""",
175+
"""
176+
/**
177+
* Downcall to Swift:
178+
* {@snippet lang=swift :
179+
* public func methodB() throws -> Int64
180+
* }
181+
*/
182+
public static native long methodB() throws Exception;
183+
""",
184+
]
185+
)
186+
}
187+
188+
@Test
189+
func globalMethodThrowing_swiftThunks() throws {
190+
try assertOutput(
191+
input: globalMethodThrowing,
192+
.jni,
193+
.swift,
194+
detectChunkByInitialLines: 1,
195+
expectedChunks: [
196+
"""
197+
@_cdecl("Java_com_example_swift_SwiftModule_methodA")
198+
func swiftjava_SwiftModule_methodA(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass) {
199+
do {
200+
try SwiftModule.methodA()
201+
} catch {
202+
environment.throwAsException(error)
203+
}
204+
}
205+
""",
206+
"""
207+
@_cdecl("Java_com_example_swift_SwiftModule_methodB")
208+
func swiftjava_SwiftModule_methodB(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass) -> jlong {
209+
do {
210+
let result = try SwiftModule.methodB()
211+
return result.getJNIValue(in: environment)
212+
} catch {
213+
environment.throwAsException(error)
214+
return Int64.jniPlaceholderValue
215+
}
216+
}
217+
""",
218+
]
219+
)
220+
}
153221
}

0 commit comments

Comments
 (0)