Skip to content

Commit 18eaf93

Browse files
committed
BridgeJS: Support non-ConvertibleToJSValue async exported return types
1 parent be3b300 commit 18eaf93

67 files changed

Lines changed: 3939 additions & 175 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 124 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,77 @@ public class ExportSwift {
9090
decls.append(contentsOf: try renderSingleExportedClass(klass: klass))
9191
}
9292
}
93+
94+
try withSpan("Render Async Promise Helpers") { [self] in
95+
let asyncResolveTypes = skeleton.asyncPromiseResolveReturnTypes
96+
if !asyncResolveTypes.isEmpty {
97+
decls.append(contentsOf: try renderPromiseRejectHelper())
98+
for type in asyncResolveTypes {
99+
decls.append(contentsOf: try renderPromiseResolveHelper(type))
100+
}
101+
}
102+
}
93103
return withSpan("Format Export Glue") {
94104
return decls.map { $0.description }.joined(separator: "\n\n")
95105
}
96106
}
97107

108+
/// Generates the per-type `Promise_resolve_<mangled>` settlement helper.
109+
private func renderPromiseResolveHelper(_ type: BridgeType) throws -> [DeclSyntax] {
110+
try renderPromiseSettleHelper(
111+
functionName: "Promise_resolve_\(type.mangleTypeName)",
112+
externName: "promise_resolve_\(moduleName)_\(type.mangleTypeName)",
113+
valueType: type
114+
)
115+
}
116+
117+
/// Generates the shared `Promise_reject` settlement helper.
118+
private func renderPromiseRejectHelper() throws -> [DeclSyntax] {
119+
try renderPromiseSettleHelper(
120+
functionName: "Promise_reject",
121+
externName: "promise_reject_\(moduleName)",
122+
valueType: .jsValue
123+
)
124+
}
125+
126+
/// Generates a `@JSFunction func <functionName>(_ promise: JSObject, _ value: T)` and its
127+
/// glue, lowering `value` through the standard imported-parameter ABI.
128+
private func renderPromiseSettleHelper(
129+
functionName: String,
130+
externName: String,
131+
valueType: BridgeType
132+
) throws -> [DeclSyntax] {
133+
let effects = Effects(isAsync: false, isThrows: true)
134+
// `Void` can't cross the bridge as a parameter, so the void helper takes only the promise.
135+
var parameters = [Parameter(label: nil, name: "promise", type: .jsObject(nil))]
136+
if valueType != .void {
137+
parameters.append(Parameter(label: nil, name: "value", type: valueType))
138+
}
139+
let builder = try ImportTS.CallJSEmission(
140+
moduleName: "bjs",
141+
abiName: externName,
142+
effects: effects,
143+
returnType: .void,
144+
context: .importTS
145+
)
146+
for parameter in parameters {
147+
try builder.lowerParameter(param: parameter)
148+
}
149+
try builder.call()
150+
try builder.liftReturnValue()
151+
152+
let valueParam = valueType == .void ? "" : ", _ value: \(valueType.swiftType)"
153+
let macroDecl: DeclSyntax =
154+
"@JSFunction func \(raw: functionName)(_ promise: JSObject\(raw: valueParam)) throws(JSException)"
155+
let glueDecl = builder.renderThunkDecl(
156+
name: "_$\(functionName)",
157+
parameters: parameters,
158+
returnType: .void,
159+
effects: effects
160+
)
161+
return [macroDecl, builder.renderImportDecl(), glueDecl]
162+
}
163+
98164
class ExportedThunkBuilder {
99165
var body: [CodeBlockItemSyntax] = []
100166
var liftedParameterExprs: [ExprSyntax] = []
@@ -104,8 +170,22 @@ public class ExportSwift {
104170
var externDecls: [DeclSyntax] = []
105171
let effects: Effects
106172

107-
init(effects: Effects) {
173+
/// The async return type settled through `_bjs_makePromise`'s `Promise_resolve_<mangled>`
174+
/// helper. Set for every `async` thunk.
175+
var asyncResolveReturnType: BridgeType?
176+
177+
/// Stack-using parameter lifts hoisted ahead of the deferred async closure.
178+
var asyncHoistedBindings: [CodeBlockItemSyntax] = []
179+
180+
init(effects: Effects, returnType: BridgeType) throws {
108181
self.effects = effects
182+
guard effects.isAsync else { return }
183+
guard returnType.isAsyncResolvable else {
184+
throw BridgeJSCoreError(
185+
"Returning '\(returnType.swiftType)' from an async exported function is not yet supported"
186+
)
187+
}
188+
self.asyncResolveReturnType = returnType
109189
}
110190

111191
private func append(_ item: CodeBlockItemSyntax) {
@@ -200,7 +280,7 @@ public class ExportSwift {
200280
}
201281

202282
if effects.isAsync, returnType != .void {
203-
return CodeBlockItemSyntax(item: .init(StmtSyntax("return \(raw: callExpr).jsValue")))
283+
return CodeBlockItemSyntax(item: .init(StmtSyntax("return \(raw: callExpr)")))
204284
}
205285

206286
if returnType == .void {
@@ -244,6 +324,22 @@ public class ExportSwift {
244324
param.type.isStackUsingParameter ? index : nil
245325
}
246326

327+
if effects.isAsync {
328+
// Drain stack parameters before the deferred `Task` or the shared stack is corrupted.
329+
for index in stackParamIndices.reversed() {
330+
let param = parameters[index]
331+
let expr = liftedParameterExprs[index]
332+
let varName = "_tmp_\(param.name)"
333+
var binding: CodeBlockItemSyntax = "let \(raw: varName) = \(expr)"
334+
if !asyncHoistedBindings.isEmpty {
335+
binding = binding.with(\.leadingTrivia, .newline)
336+
}
337+
asyncHoistedBindings.append(binding)
338+
liftedParameterExprs[index] = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varName)))
339+
}
340+
return
341+
}
342+
247343
guard stackParamIndices.count > 1 else { return }
248344

249345
for index in stackParamIndices.reversed() {
@@ -293,8 +389,7 @@ public class ExportSwift {
293389
return
294390
}
295391
if effects.isAsync {
296-
// The return value of async function (T of `(...) async -> T`) is
297-
// handled by the JSPromise.async, so we don't need to do anything here.
392+
// The async return value is lowered by the generated `Promise_resolve_*` helper.
298393
return
299394
}
300395

@@ -328,25 +423,25 @@ public class ExportSwift {
328423
}
329424
}
330425

426+
/// A throwing async body needs an explicit closure type, otherwise Swift infers
427+
/// `throws(any Error)` instead of `throws(JSException)`.
428+
/// See: https://github.com/swiftlang/swift/issues/76165
429+
private func asyncThrowsClosureHead(returnSpelling: String?) -> String {
430+
guard effects.isThrows else { return "" }
431+
let returns = returnSpelling.map { " -> \($0)" } ?? ""
432+
return " () async throws(JSException)\(returns) in"
433+
}
434+
331435
func render(abiName: String) -> DeclSyntax {
332436
let body: CodeBlockItemListSyntax
333-
if effects.isAsync {
334-
// Explicit closure type annotation needed when throws is present
335-
// so Swift infers throws(JSException) instead of throws(any Error)
336-
// See: https://github.com/swiftlang/swift/issues/76165
337-
let closureHead: String
338-
if effects.isThrows {
339-
let hasReturn = self.body.contains { $0.description.contains("return ") }
340-
let ret = hasReturn ? " -> JSValue" : ""
341-
closureHead = " () async throws(JSException)\(ret) in"
342-
} else {
343-
closureHead = ""
344-
}
437+
if effects.isAsync, let resolveType = asyncResolveReturnType {
438+
let resolveName = "Promise_resolve_\(resolveType.mangleTypeName)"
439+
let closureHead = asyncThrowsClosureHead(returnSpelling: resolveType.swiftType)
345440
body = """
346-
let ret = JSPromise.async {\(raw: closureHead)
441+
\(CodeBlockItemListSyntax(asyncHoistedBindings))
442+
return _bjs_makePromise(resolve: \(raw: resolveName), reject: Promise_reject) {\(raw: closureHead)
347443
\(CodeBlockItemListSyntax(self.body))
348-
}.jsObject
349-
return ret.bridgeJSLowerReturn()
444+
}
350445
"""
351446
} else if effects.isThrows {
352447
body = """
@@ -457,7 +552,10 @@ public class ExportSwift {
457552
let className = context.className
458553
let isStatic = context.isStatic
459554

460-
let getterBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false, isStatic: isStatic))
555+
let getterBuilder = try ExportedThunkBuilder(
556+
effects: Effects(isAsync: false, isThrows: false, isStatic: isStatic),
557+
returnType: property.type
558+
)
461559

462560
if !isStatic {
463561
try getterBuilder.liftParameter(
@@ -476,8 +574,9 @@ public class ExportSwift {
476574

477575
// Generate property setter if not readonly
478576
if !property.isReadonly {
479-
let setterBuilder = ExportedThunkBuilder(
480-
effects: Effects(isAsync: false, isThrows: false, isStatic: isStatic)
577+
let setterBuilder = try ExportedThunkBuilder(
578+
effects: Effects(isAsync: false, isThrows: false, isStatic: isStatic),
579+
returnType: .void
481580
)
482581

483582
// Lift parameters based on property type
@@ -507,7 +606,7 @@ public class ExportSwift {
507606
}
508607

509608
func renderSingleExportedFunction(function: ExportedFunction) throws -> DeclSyntax {
510-
let builder = ExportedThunkBuilder(effects: function.effects)
609+
let builder = try ExportedThunkBuilder(effects: function.effects, returnType: function.returnType)
511610
for param in function.parameters {
512611
try builder.liftParameter(param: param)
513612
}
@@ -536,7 +635,7 @@ public class ExportSwift {
536635
callName: String,
537636
returnType: BridgeType
538637
) throws -> DeclSyntax {
539-
let builder = ExportedThunkBuilder(effects: constructor.effects)
638+
let builder = try ExportedThunkBuilder(effects: constructor.effects, returnType: returnType)
540639
for param in constructor.parameters {
541640
try builder.liftParameter(param: param)
542641
}
@@ -550,7 +649,7 @@ public class ExportSwift {
550649
ownerTypeName: String,
551650
instanceSelfType: BridgeType
552651
) throws -> DeclSyntax {
553-
let builder = ExportedThunkBuilder(effects: method.effects)
652+
let builder = try ExportedThunkBuilder(effects: method.effects, returnType: method.returnType)
554653
if !method.effects.isStatic {
555654
try builder.liftParameter(param: Parameter(label: nil, name: "_self", type: instanceSelfType))
556655
}

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,40 @@ public struct BridgeJSLink {
356356
]
357357
}
358358

359+
/// JS const (in the import glue scope) holding the `Symbol` under which a promise's
360+
/// resolve/reject settlers are stashed.
361+
private static let promiseSettlersSymbol = "__bjs_promiseSettlers"
362+
363+
/// Renders a `bjs[...]` settlement handler that lifts `(promise, value)` and calls the
364+
/// promise's stashed `resolve` / `reject` settler.
365+
private func renderPromiseSettleHandler(
366+
externName: String,
367+
valueType: BridgeType,
368+
settle: String,
369+
into printer: CodeFragmentPrinter
370+
) throws {
371+
let builder = ImportedThunkBuilder(
372+
effects: Effects(isAsync: false, isThrows: true),
373+
returnType: .void,
374+
intrinsicRegistry: intrinsicRegistry
375+
)
376+
try builder.liftParameter(param: Parameter(label: nil, name: "promise", type: .jsObject(nil)))
377+
// `Void` can't cross the bridge as a parameter, so the void resolve settles with `undefined`.
378+
let valueArg: String
379+
if valueType == .void {
380+
valueArg = ""
381+
} else {
382+
try builder.liftParameter(param: Parameter(label: nil, name: "value", type: valueType))
383+
valueArg = builder.parameterForwardings[1]
384+
}
385+
builder.body.write(
386+
"\(builder.parameterForwardings[0])[\(Self.promiseSettlersSymbol)].\(settle)(\(valueArg));"
387+
)
388+
var lines = builder.renderFunction(name: nil)
389+
lines[0] = "bjs[\"\(externName)\"] = \(lines[0])"
390+
printer.write(lines: lines)
391+
}
392+
359393
private func generateAddImports(needsImportsObject: Bool) throws -> CodeFragmentPrinter {
360394
let printer = CodeFragmentPrinter()
361395
let allStructs = skeletons.compactMap { $0.exported?.structs }.flatMap { $0 }
@@ -526,6 +560,39 @@ public struct BridgeJSLink {
526560
}
527561
}
528562

563+
// Always provided: the runtime's `_bjs_makePromise` imports it unconditionally.
564+
// The settlers are stored under a Symbol to avoid clashing with promise fields.
565+
printer.write("const \(Self.promiseSettlersSymbol) = Symbol(\"JavaScriptKit.promiseSettlers\");")
566+
printer.write("bjs[\"swift_js_make_promise\"] = function() {")
567+
printer.indent {
568+
printer.write("let resolve, reject;")
569+
printer.write("const promise = new Promise((res, rej) => { resolve = res; reject = rej; });")
570+
printer.write("promise[\(Self.promiseSettlersSymbol)] = { resolve, reject };")
571+
printer.write(
572+
"return \(JSGlueVariableScope.reservedSwift).\(JSGlueVariableScope.reservedMemory).retain(promise);"
573+
)
574+
}
575+
printer.write("}")
576+
for skeleton in skeletons {
577+
guard let exported = skeleton.exported else { continue }
578+
let asyncResolveTypes = exported.asyncPromiseResolveReturnTypes
579+
guard !asyncResolveTypes.isEmpty else { continue }
580+
for type in asyncResolveTypes {
581+
try renderPromiseSettleHandler(
582+
externName: "promise_resolve_\(skeleton.moduleName)_\(type.mangleTypeName)",
583+
valueType: type,
584+
settle: "resolve",
585+
into: printer
586+
)
587+
}
588+
try renderPromiseSettleHandler(
589+
externName: "promise_reject_\(skeleton.moduleName)",
590+
valueType: .jsValue,
591+
settle: "reject",
592+
into: printer
593+
)
594+
}
595+
529596
printer.write("bjs[\"swift_js_return_optional_bool\"] = function(isSome, value) {")
530597
printer.indent {
531598
printer.write("if (isSome === 0) {")

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,30 @@ public struct ExportedSkeleton: Codable {
10271027
public var isEmpty: Bool {
10281028
functions.isEmpty && classes.isEmpty && enums.isEmpty && structs.isEmpty && protocols.isEmpty
10291029
}
1030+
1031+
/// Distinct `async` return types needing a `Promise_resolve_<mangled>` helper, deduplicated
1032+
/// by mangled name. Shared by the Swift codegen and JS link.
1033+
public var asyncPromiseResolveReturnTypes: [BridgeType] {
1034+
var seen = Set<String>()
1035+
var result: [BridgeType] = []
1036+
func consider(_ returnType: BridgeType, _ effects: Effects) {
1037+
guard effects.isAsync, returnType.isAsyncResolvable,
1038+
seen.insert(returnType.mangleTypeName).inserted
1039+
else { return }
1040+
result.append(returnType)
1041+
}
1042+
for function in functions { consider(function.returnType, function.effects) }
1043+
for klass in classes {
1044+
for method in klass.methods { consider(method.returnType, method.effects) }
1045+
}
1046+
for structDef in structs {
1047+
for method in structDef.methods { consider(method.returnType, method.effects) }
1048+
}
1049+
for enumDef in enums {
1050+
for method in enumDef.staticMethods { consider(method.returnType, method.effects) }
1051+
}
1052+
return result
1053+
}
10301054
}
10311055

10321056
// MARK: - Imported Skeleton
@@ -1584,6 +1608,25 @@ extension BridgeType {
15841608
return false
15851609
}
15861610

1611+
/// Whether a value of this type can be passed to a generated `Promise_resolve_<mangled>`
1612+
/// settlement helper, i.e. lowered through the imported-parameter ABI. Every `async`
1613+
/// exported return settles through `_bjs_makePromise`; the few types that cannot be lowered
1614+
/// (associated-value enums, protocols, namespace enums, and their compositions) are diagnosed.
1615+
public var isAsyncResolvable: Bool {
1616+
switch self {
1617+
case .associatedValueEnum, .swiftProtocol, .namespaceEnum:
1618+
return false
1619+
case .nullable(let wrapped, _):
1620+
return wrapped.isAsyncResolvable
1621+
case .array(let element):
1622+
return element.isAsyncResolvable
1623+
case .dictionary(let value):
1624+
return value.isAsyncResolvable
1625+
default:
1626+
return true
1627+
}
1628+
}
1629+
15871630
/// Simplified Swift ABI-style mangled name
15881631
/// https://github.com/swiftlang/swift/blob/main/docs/ABI/Mangling.rst#types
15891632
public var mangleTypeName: String {

0 commit comments

Comments
 (0)