Skip to content

BridgeJS: Add throws(JSException) to imported methods #390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 23, 2025
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
15 changes: 12 additions & 3 deletions Benchmarks/Sources/Generated/BridgeJS.ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

@_spi(BridgeJS) import JavaScriptKit

func benchmarkHelperNoop() -> Void {
func benchmarkHelperNoop() throws(JSException) -> Void {
#if arch(wasm32)
@_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoop")
func bjs_benchmarkHelperNoop() -> Void
Expand All @@ -16,9 +16,12 @@ func benchmarkHelperNoop() -> Void {
}
#endif
bjs_benchmarkHelperNoop()
if let error = _swift_js_take_exception() {
throw error
}
}

func benchmarkHelperNoopWithNumber(_ n: Double) -> Void {
func benchmarkHelperNoopWithNumber(_ n: Double) throws(JSException) -> Void {
#if arch(wasm32)
@_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoopWithNumber")
func bjs_benchmarkHelperNoopWithNumber(_ n: Float64) -> Void
Expand All @@ -28,9 +31,12 @@ func benchmarkHelperNoopWithNumber(_ n: Double) -> Void {
}
#endif
bjs_benchmarkHelperNoopWithNumber(n)
if let error = _swift_js_take_exception() {
throw error
}
}

func benchmarkRunner(_ name: String, _ body: JSObject) -> Void {
func benchmarkRunner(_ name: String, _ body: JSObject) throws(JSException) -> Void {
#if arch(wasm32)
@_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkRunner")
func bjs_benchmarkRunner(_ name: Int32, _ body: Int32) -> Void
Expand All @@ -44,4 +50,7 @@ func benchmarkRunner(_ name: String, _ body: JSObject) -> Void {
_swift_js_make_js_string(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
}
bjs_benchmarkRunner(nameId, Int32(bitPattern: body.id))
if let error = _swift_js_take_exception() {
throw error
}
}
5 changes: 5 additions & 0 deletions Examples/PlayBridgeJS/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ let package = Package(
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
],
exclude: [
"bridge-js.d.ts",
"bridge-js.config.json",
"Generated/JavaScript",
],
swiftSettings: [
.enableExperimentalFeature("Extern")
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ public func _bjs_PlayBridgeJS_update(_self: UnsafeMutableRawPointer, swiftSource
_swift_js_init_memory(swiftSourceBytes, b.baseAddress.unsafelyUnwrapped)
return Int(swiftSourceLen)
}
let dtsSource = String(unsafeUninitializedCapacity: Int(dtsSourceLen)) { b in
let dtsSource = String(unsafeUninitializedCapacity: Int(dtsSourceLen)) { b in
_swift_js_init_memory(dtsSourceBytes, b.baseAddress.unsafelyUnwrapped)
return Int(dtsSourceLen)
}
let ret = try Unmanaged<PlayBridgeJS>.fromOpaque(_self).takeUnretainedValue().update(swiftSource: swiftSource, dtsSource: dtsSource)
return Unmanaged.passRetained(ret).toOpaque()
let ret = try Unmanaged<PlayBridgeJS>.fromOpaque(_self).takeUnretainedValue().update(swiftSource: swiftSource, dtsSource: dtsSource)
return Unmanaged.passRetained(ret).toOpaque()
} catch let error {
if let error = error.thrownValue.object {
withExtendedLifetime(error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

@_spi(BridgeJS) import JavaScriptKit

func createTS2Skeleton() -> TS2Skeleton {
func createTS2Skeleton() throws(JSException) -> TS2Skeleton {
#if arch(wasm32)
@_extern(wasm, module: "PlayBridgeJS", name: "bjs_createTS2Skeleton")
func bjs_createTS2Skeleton() -> Int32
Expand All @@ -16,6 +16,9 @@ func createTS2Skeleton() -> TS2Skeleton {
}
#endif
let ret = bjs_createTS2Skeleton()
if let error = _swift_js_take_exception() {
throw error
}
return TS2Skeleton(takingThis: ret)
}

Expand All @@ -30,7 +33,7 @@ struct TS2Skeleton {
self.this = JSObject(id: UInt32(bitPattern: this))
}

func convert(_ ts: String) -> String {
func convert(_ ts: String) throws(JSException) -> String {
#if arch(wasm32)
@_extern(wasm, module: "PlayBridgeJS", name: "bjs_TS2Skeleton_convert")
func bjs_TS2Skeleton_convert(_ self: Int32, _ ts: Int32) -> Int32
Expand All @@ -44,6 +47,9 @@ struct TS2Skeleton {
_swift_js_make_js_string(b.baseAddress.unsafelyUnwrapped, Int32(b.count))
}
let ret = bjs_TS2Skeleton_convert(Int32(bitPattern: self.this.id), tsId)
if let error = _swift_js_take_exception() {
throw error
}
return String(unsafeUninitializedCapacity: Int(ret)) { b in
_swift_js_init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret))
return Int(ret)
Expand Down
6 changes: 4 additions & 2 deletions Examples/PlayBridgeJS/Sources/PlayBridgeJS/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import class Foundation.JSONDecoder
@JS func update(swiftSource: String, dtsSource: String) throws(JSException) -> PlayBridgeJSOutput {
do {
return try _update(swiftSource: swiftSource, dtsSource: dtsSource)
} catch let error as JSException {
throw error
} catch {
throw JSException(message: String(describing: error))
}
Expand All @@ -20,8 +22,8 @@ import class Foundation.JSONDecoder
try exportSwift.addSourceFile(sourceFile, "Playground.swift")
let exportResult = try exportSwift.finalize()
var importTS = ImportTS(progress: .silent, moduleName: "Playground")
let ts2skeleton = createTS2Skeleton()
let skeletonJSONString = ts2skeleton.convert(dtsSource)
let ts2skeleton = try createTS2Skeleton()
let skeletonJSONString = try ts2skeleton.convert(dtsSource)
let decoder = JSONDecoder()
let importSkeleton = try decoder.decode(
ImportedFileSkeleton.self,
Expand Down
9 changes: 7 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,17 @@ let package = Package(
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
],
path: "Plugins/BridgeJS/Sources/BridgeJSTool"
path: "Plugins/BridgeJS/Sources/BridgeJSTool",
exclude: ["TS2Skeleton/JavaScript"]
),
.testTarget(
name: "BridgeJSRuntimeTests",
dependencies: ["JavaScriptKit"],
exclude: ["Generated/JavaScript"],
exclude: [
"bridge-js.config.json",
"bridge-js.d.ts",
"Generated/JavaScript",
],
swiftSettings: [
.enableExperimentalFeature("Extern")
]
Expand Down
37 changes: 13 additions & 24 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ class ExportSwift {

fileprivate final class APICollector: SyntaxAnyVisitor {
var exportedFunctions: [ExportedFunction] = []
var exportedClasses: [String: ExportedClass] = [:]
/// The names of the exported classes, in the order they were written in the source file
var exportedClassNames: [String] = []
var exportedClassByName: [String: ExportedClass] = [:]
var errors: [DiagnosticError] = []

enum State {
Expand Down Expand Up @@ -114,7 +116,7 @@ class ExportSwift {
return .skipChildren
case .classBody(let name):
if let exportedFunction = visitFunction(node: node) {
exportedClasses[name]?.methods.append(exportedFunction)
exportedClassByName[name]?.methods.append(exportedFunction)
}
return .skipChildren
}
Expand Down Expand Up @@ -217,7 +219,7 @@ class ExportSwift {
parameters: parameters,
effects: effects
)
exportedClasses[name]?.constructor = constructor
exportedClassByName[name]?.constructor = constructor
return .skipChildren
}

Expand All @@ -226,11 +228,12 @@ class ExportSwift {
stateStack.push(state: .classBody(name: name))

guard node.attributes.hasJSAttribute() else { return .skipChildren }
exportedClasses[name] = ExportedClass(
exportedClassByName[name] = ExportedClass(
name: name,
constructor: nil,
methods: []
)
exportedClassNames.append(name)
return .visitChildren
}
override func visitPost(_ node: ClassDeclSyntax) {
Expand All @@ -242,7 +245,11 @@ class ExportSwift {
let collector = APICollector(parent: self)
collector.walk(sourceFile)
exportedFunctions.append(contentsOf: collector.exportedFunctions)
exportedClasses.append(contentsOf: collector.exportedClasses.values)
exportedClasses.append(
contentsOf: collector.exportedClassNames.map {
collector.exportedClassByName[$0]!
}
)
return collector.errors
}

Expand Down Expand Up @@ -426,25 +433,7 @@ class ExportSwift {
}

func lowerReturnValue(returnType: BridgeType) {
switch returnType {
case .void:
abiReturnType = nil
case .bool:
abiReturnType = .i32
case .int:
abiReturnType = .i32
case .float:
abiReturnType = .f32
case .double:
abiReturnType = .f64
case .string:
abiReturnType = nil
case .jsObject:
abiReturnType = .i32
case .swiftHeapObject:
// UnsafeMutableRawPointer is returned as an i32 pointer
abiReturnType = .pointer
}
abiReturnType = returnType.abiReturnType

switch returnType {
case .void: break
Expand Down
66 changes: 50 additions & 16 deletions Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ struct ImportTS {
} else {
body.append("let ret = \(raw: call)")
}
body.append("if let error = _swift_js_take_exception() { throw error }")
}

func liftReturnValue(returnType: BridgeType) throws {
Expand Down Expand Up @@ -253,6 +254,7 @@ struct ImportTS {
)
}
}),
effectSpecifiers: ImportTS.buildFunctionEffect(throws: true, async: false),
returnClause: ReturnClauseSyntax(
arrow: .arrowToken(),
type: IdentifierTypeSyntax(name: .identifier(returnType.swiftType))
Expand Down Expand Up @@ -280,7 +282,8 @@ struct ImportTS {
)
}
}
)
),
effectSpecifiers: ImportTS.buildFunctionEffect(throws: true, async: false)
),
bodyBuilder: {
self.renderImportDecl()
Expand Down Expand Up @@ -364,38 +367,33 @@ struct ImportTS {
try builder.liftReturnValue(returnType: property.type)
return AccessorDeclSyntax(
accessorSpecifier: .keyword(.get),
effectSpecifiers: Self.buildAccessorEffect(throws: true, async: false),
body: CodeBlockSyntax {
builder.renderImportDecl()
builder.body
}
)
}

func renderSetterDecl(property: ImportedPropertySkeleton) throws -> AccessorDeclSyntax {
func renderSetterDecl(property: ImportedPropertySkeleton) throws -> DeclSyntax {
let builder = ImportedThunkBuilder(
moduleName: moduleName,
abiName: property.setterAbiName(context: type)
)
let newValue = Parameter(label: nil, name: "newValue", type: property.type)
try builder.lowerParameter(param: Parameter(label: nil, name: "self", type: .jsObject(name)))
try builder.lowerParameter(param: Parameter(label: nil, name: "newValue", type: property.type))
try builder.lowerParameter(param: newValue)
builder.call(returnType: .void)
return AccessorDeclSyntax(
modifier: DeclModifierSyntax(name: .keyword(.nonmutating)),
accessorSpecifier: .keyword(.set),
body: CodeBlockSyntax {
builder.renderImportDecl()
builder.body
}
return builder.renderThunkDecl(
name: "set\(property.name.capitalizedFirstLetter())",
parameters: [newValue],
returnType: .void
)
}

func renderPropertyDecl(property: ImportedPropertySkeleton) throws -> [DeclSyntax] {
var accessorDecls: [AccessorDeclSyntax] = []
accessorDecls.append(try renderGetterDecl(property: property))
if !property.isReadonly {
accessorDecls.append(try renderSetterDecl(property: property))
}
return [
let accessorDecls: [AccessorDeclSyntax] = [try renderGetterDecl(property: property)]
var decls: [DeclSyntax] = [
DeclSyntax(
VariableDeclSyntax(
leadingTrivia: Self.renderDocumentation(documentation: property.documentation),
Expand All @@ -418,6 +416,10 @@ struct ImportTS {
)
)
]
if !property.isReadonly {
decls.append(try renderSetterDecl(property: property))
}
return decls
}
let classDecl = try StructDeclSyntax(
leadingTrivia: Self.renderDocumentation(documentation: type.documentation),
Expand Down Expand Up @@ -469,4 +471,36 @@ struct ImportTS {
let lines = documentation.split { $0.isNewline }
return Trivia(pieces: lines.flatMap { [TriviaPiece.docLineComment("/// \($0)"), .newlines(1)] })
}

static func buildFunctionEffect(throws: Bool, async: Bool) -> FunctionEffectSpecifiersSyntax {
return FunctionEffectSpecifiersSyntax(
asyncSpecifier: `async` ? .keyword(.async) : nil,
throwsClause: `throws`
? ThrowsClauseSyntax(
throwsSpecifier: .keyword(.throws),
leftParen: .leftParenToken(),
type: IdentifierTypeSyntax(name: .identifier("JSException")),
rightParen: .rightParenToken()
) : nil
)
}
static func buildAccessorEffect(throws: Bool, async: Bool) -> AccessorEffectSpecifiersSyntax {
return AccessorEffectSpecifiersSyntax(
asyncSpecifier: `async` ? .keyword(.async) : nil,
throwsClause: `throws`
? ThrowsClauseSyntax(
throwsSpecifier: .keyword(.throws),
leftParen: .leftParenToken(),
type: IdentifierTypeSyntax(name: .identifier("JSException")),
rightParen: .rightParenToken()
) : nil
)
}
}

extension String {
func capitalizedFirstLetter() -> String {
guard !isEmpty else { return self }
return prefix(1).uppercased() + dropFirst()
}
}
Loading