Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
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
149 changes: 124 additions & 25 deletions 149 Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,77 @@ public class ExportSwift {
decls.append(contentsOf: try renderSingleExportedClass(klass: klass))
}
}

try withSpan("Render Async Promise Helpers") { [self] in
let asyncResolveTypes = skeleton.asyncPromiseResolveReturnTypes
if !asyncResolveTypes.isEmpty {
decls.append(contentsOf: try renderPromiseRejectHelper())
for type in asyncResolveTypes {
decls.append(contentsOf: try renderPromiseResolveHelper(type))
}
}
}
return withSpan("Format Export Glue") {
return decls.map { $0.description }.joined(separator: "\n\n")
}
}

/// Generates the per-type `Promise_resolve_<mangled>` settlement helper.
private func renderPromiseResolveHelper(_ type: BridgeType) throws -> [DeclSyntax] {
try renderPromiseSettleHelper(
functionName: "Promise_resolve_\(type.mangleTypeName)",
externName: "promise_resolve_\(moduleName)_\(type.mangleTypeName)",
valueType: type
)
}

/// Generates the shared `Promise_reject` settlement helper.
private func renderPromiseRejectHelper() throws -> [DeclSyntax] {
try renderPromiseSettleHelper(
functionName: "Promise_reject",
externName: "promise_reject_\(moduleName)",
valueType: .jsValue
)
}

/// Generates a `@JSFunction func <functionName>(_ promise: JSObject, _ value: T)` and its
/// glue, lowering `value` through the standard imported-parameter ABI.
private func renderPromiseSettleHelper(
functionName: String,
externName: String,
valueType: BridgeType
) throws -> [DeclSyntax] {
let effects = Effects(isAsync: false, isThrows: true)
// `Void` can't cross the bridge as a parameter, so the void helper takes only the promise.
var parameters = [Parameter(label: nil, name: "promise", type: .jsObject(nil))]
if valueType != .void {
parameters.append(Parameter(label: nil, name: "value", type: valueType))
}
let builder = try ImportTS.CallJSEmission(
moduleName: "bjs",
abiName: externName,
effects: effects,
returnType: .void,
context: .importTS
)
for parameter in parameters {
try builder.lowerParameter(param: parameter)
}
try builder.call()
try builder.liftReturnValue()

let valueParam = valueType == .void ? "" : ", _ value: \(valueType.swiftType)"
let macroDecl: DeclSyntax =
"@JSFunction func \(raw: functionName)(_ promise: JSObject\(raw: valueParam)) throws(JSException)"
let glueDecl = builder.renderThunkDecl(
name: "_$\(functionName)",
parameters: parameters,
returnType: .void,
effects: effects
)
return [macroDecl, builder.renderImportDecl(), glueDecl]
}

class ExportedThunkBuilder {
var body: [CodeBlockItemSyntax] = []
var liftedParameterExprs: [ExprSyntax] = []
Expand All @@ -104,8 +170,22 @@ public class ExportSwift {
var externDecls: [DeclSyntax] = []
let effects: Effects

init(effects: Effects) {
/// The async return type settled through `_bjs_makePromise`'s `Promise_resolve_<mangled>`
/// helper. Set for every `async` thunk.
var asyncResolveReturnType: BridgeType?

/// Stack-using parameter lifts hoisted ahead of the deferred async closure.
var asyncHoistedBindings: [CodeBlockItemSyntax] = []

init(effects: Effects, returnType: BridgeType) throws {
self.effects = effects
guard effects.isAsync else { return }
guard returnType.isAsyncResolvable else {
throw BridgeJSCoreError(
"Returning '\(returnType.swiftType)' from an async exported function is not yet supported"
)
}
self.asyncResolveReturnType = returnType
}

private func append(_ item: CodeBlockItemSyntax) {
Expand Down Expand Up @@ -200,7 +280,7 @@ public class ExportSwift {
}

if effects.isAsync, returnType != .void {
return CodeBlockItemSyntax(item: .init(StmtSyntax("return \(raw: callExpr).jsValue")))
return CodeBlockItemSyntax(item: .init(StmtSyntax("return \(raw: callExpr)")))
}

if returnType == .void {
Expand Down Expand Up @@ -244,6 +324,22 @@ public class ExportSwift {
param.type.isStackUsingParameter ? index : nil
}

if effects.isAsync {
// Drain stack parameters before the deferred `Task` or the shared stack is corrupted.
for index in stackParamIndices.reversed() {
let param = parameters[index]
let expr = liftedParameterExprs[index]
let varName = "_tmp_\(param.name)"
var binding: CodeBlockItemSyntax = "let \(raw: varName) = \(expr)"
if !asyncHoistedBindings.isEmpty {
binding = binding.with(\.leadingTrivia, .newline)
}
asyncHoistedBindings.append(binding)
liftedParameterExprs[index] = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(varName)))
}
return
}

guard stackParamIndices.count > 1 else { return }

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

Expand Down Expand Up @@ -328,25 +423,25 @@ public class ExportSwift {
}
}

/// A throwing async body needs an explicit closure type, otherwise Swift infers
/// `throws(any Error)` instead of `throws(JSException)`.
/// See: https://github.com/swiftlang/swift/issues/76165
private func asyncThrowsClosureHead(returnSpelling: String?) -> String {
guard effects.isThrows else { return "" }
let returns = returnSpelling.map { " -> \($0)" } ?? ""
return " () async throws(JSException)\(returns) in"
}

func render(abiName: String) -> DeclSyntax {
let body: CodeBlockItemListSyntax
if effects.isAsync {
// Explicit closure type annotation needed when throws is present
// so Swift infers throws(JSException) instead of throws(any Error)
// See: https://github.com/swiftlang/swift/issues/76165
let closureHead: String
if effects.isThrows {
let hasReturn = self.body.contains { $0.description.contains("return ") }
let ret = hasReturn ? " -> JSValue" : ""
closureHead = " () async throws(JSException)\(ret) in"
} else {
closureHead = ""
}
if effects.isAsync, let resolveType = asyncResolveReturnType {
let resolveName = "Promise_resolve_\(resolveType.mangleTypeName)"
let closureHead = asyncThrowsClosureHead(returnSpelling: resolveType.swiftType)
body = """
let ret = JSPromise.async {\(raw: closureHead)
\(CodeBlockItemListSyntax(asyncHoistedBindings))
return _bjs_makePromise(resolve: \(raw: resolveName), reject: Promise_reject) {\(raw: closureHead)
\(CodeBlockItemListSyntax(self.body))
}.jsObject
return ret.bridgeJSLowerReturn()
}
"""
} else if effects.isThrows {
body = """
Expand Down Expand Up @@ -457,7 +552,10 @@ public class ExportSwift {
let className = context.className
let isStatic = context.isStatic

let getterBuilder = ExportedThunkBuilder(effects: Effects(isAsync: false, isThrows: false, isStatic: isStatic))
let getterBuilder = try ExportedThunkBuilder(
effects: Effects(isAsync: false, isThrows: false, isStatic: isStatic),
returnType: property.type
)

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

// Generate property setter if not readonly
if !property.isReadonly {
let setterBuilder = ExportedThunkBuilder(
effects: Effects(isAsync: false, isThrows: false, isStatic: isStatic)
let setterBuilder = try ExportedThunkBuilder(
effects: Effects(isAsync: false, isThrows: false, isStatic: isStatic),
returnType: .void
)

// Lift parameters based on property type
Expand Down Expand Up @@ -507,7 +606,7 @@ public class ExportSwift {
}

func renderSingleExportedFunction(function: ExportedFunction) throws -> DeclSyntax {
let builder = ExportedThunkBuilder(effects: function.effects)
let builder = try ExportedThunkBuilder(effects: function.effects, returnType: function.returnType)
for param in function.parameters {
try builder.liftParameter(param: param)
}
Expand Down Expand Up @@ -536,7 +635,7 @@ public class ExportSwift {
callName: String,
returnType: BridgeType
) throws -> DeclSyntax {
let builder = ExportedThunkBuilder(effects: constructor.effects)
let builder = try ExportedThunkBuilder(effects: constructor.effects, returnType: returnType)
for param in constructor.parameters {
try builder.liftParameter(param: param)
}
Expand All @@ -550,7 +649,7 @@ public class ExportSwift {
ownerTypeName: String,
instanceSelfType: BridgeType
) throws -> DeclSyntax {
let builder = ExportedThunkBuilder(effects: method.effects)
let builder = try ExportedThunkBuilder(effects: method.effects, returnType: method.returnType)
if !method.effects.isStatic {
try builder.liftParameter(param: Parameter(label: nil, name: "_self", type: instanceSelfType))
}
Expand Down
67 changes: 67 additions & 0 deletions 67 Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,40 @@ public struct BridgeJSLink {
]
}

/// JS const (in the import glue scope) holding the `Symbol` under which a promise's
/// resolve/reject settlers are stashed.
private static let promiseSettlersSymbol = "__bjs_promiseSettlers"

/// Renders a `bjs[...]` settlement handler that lifts `(promise, value)` and calls the
/// promise's stashed `resolve` / `reject` settler.
private func renderPromiseSettleHandler(
externName: String,
valueType: BridgeType,
settle: String,
into printer: CodeFragmentPrinter
) throws {
let builder = ImportedThunkBuilder(
effects: Effects(isAsync: false, isThrows: true),
returnType: .void,
intrinsicRegistry: intrinsicRegistry
)
try builder.liftParameter(param: Parameter(label: nil, name: "promise", type: .jsObject(nil)))
// `Void` can't cross the bridge as a parameter, so the void resolve settles with `undefined`.
let valueArg: String
if valueType == .void {
valueArg = ""
} else {
try builder.liftParameter(param: Parameter(label: nil, name: "value", type: valueType))
valueArg = builder.parameterForwardings[1]
}
builder.body.write(
"\(builder.parameterForwardings[0])[\(Self.promiseSettlersSymbol)].\(settle)(\(valueArg));"
)
var lines = builder.renderFunction(name: nil)
lines[0] = "bjs[\"\(externName)\"] = \(lines[0])"
printer.write(lines: lines)
}

private func generateAddImports(needsImportsObject: Bool) throws -> CodeFragmentPrinter {
let printer = CodeFragmentPrinter()
let allStructs = skeletons.compactMap { $0.exported?.structs }.flatMap { $0 }
Expand Down Expand Up @@ -526,6 +560,39 @@ public struct BridgeJSLink {
}
}

// Always provided: the runtime's `_bjs_makePromise` imports it unconditionally.
// The settlers are stored under a Symbol to avoid clashing with promise fields.
printer.write("const \(Self.promiseSettlersSymbol) = Symbol(\"JavaScriptKit.promiseSettlers\");")
printer.write("bjs[\"swift_js_make_promise\"] = function() {")
printer.indent {
printer.write("let resolve, reject;")
printer.write("const promise = new Promise((res, rej) => { resolve = res; reject = rej; });")
printer.write("promise[\(Self.promiseSettlersSymbol)] = { resolve, reject };")
printer.write(
"return \(JSGlueVariableScope.reservedSwift).\(JSGlueVariableScope.reservedMemory).retain(promise);"
)
}
printer.write("}")
for skeleton in skeletons {
guard let exported = skeleton.exported else { continue }
let asyncResolveTypes = exported.asyncPromiseResolveReturnTypes
guard !asyncResolveTypes.isEmpty else { continue }
for type in asyncResolveTypes {
try renderPromiseSettleHandler(
externName: "promise_resolve_\(skeleton.moduleName)_\(type.mangleTypeName)",
valueType: type,
settle: "resolve",
into: printer
)
}
try renderPromiseSettleHandler(
externName: "promise_reject_\(skeleton.moduleName)",
valueType: .jsValue,
settle: "reject",
into: printer
)
}

printer.write("bjs[\"swift_js_return_optional_bool\"] = function(isSome, value) {")
printer.indent {
printer.write("if (isSome === 0) {")
Expand Down
43 changes: 43 additions & 0 deletions 43 Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,30 @@ public struct ExportedSkeleton: Codable {
public var isEmpty: Bool {
functions.isEmpty && classes.isEmpty && enums.isEmpty && structs.isEmpty && protocols.isEmpty
}

/// Distinct `async` return types needing a `Promise_resolve_<mangled>` helper, deduplicated
/// by mangled name. Shared by the Swift codegen and JS link.
public var asyncPromiseResolveReturnTypes: [BridgeType] {
var seen = Set<String>()
var result: [BridgeType] = []
func consider(_ returnType: BridgeType, _ effects: Effects) {
guard effects.isAsync, returnType.isAsyncResolvable,
seen.insert(returnType.mangleTypeName).inserted
else { return }
result.append(returnType)
}
for function in functions { consider(function.returnType, function.effects) }
for klass in classes {
for method in klass.methods { consider(method.returnType, method.effects) }
}
for structDef in structs {
for method in structDef.methods { consider(method.returnType, method.effects) }
}
for enumDef in enums {
for method in enumDef.staticMethods { consider(method.returnType, method.effects) }
}
return result
}
}

// MARK: - Imported Skeleton
Expand Down Expand Up @@ -1584,6 +1608,25 @@ extension BridgeType {
return false
}

/// Whether a value of this type can be passed to a generated `Promise_resolve_<mangled>`
/// settlement helper, i.e. lowered through the imported-parameter ABI. Every `async`
/// exported return settles through `_bjs_makePromise`; the few types that cannot be lowered
/// (associated-value enums, protocols, namespace enums, and their compositions) are diagnosed.
public var isAsyncResolvable: Bool {
switch self {
case .associatedValueEnum, .swiftProtocol, .namespaceEnum:
return false
case .nullable(let wrapped, _):
return wrapped.isAsyncResolvable
case .array(let element):
return element.isAsyncResolvable
case .dictionary(let value):
return value.isAsyncResolvable
default:
return true
}
}

/// Simplified Swift ABI-style mangled name
/// https://github.com/swiftlang/swift/blob/main/docs/ABI/Mangling.rst#types
public var mangleTypeName: String {
Expand Down
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.