From 0e11da502d9601712fae1c8d8d7ae6ed21676a70 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 10 Aug 2026 01:54:07 +0100 Subject: [PATCH] BridgeJS: Unify @JS struct parameter lowering onto the stack ABI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bridgeJSLowerParameter for structs used toJSObject(), so Swift→JS callbacks discarded the stack and JS lift() read garbage. Match arrays: stack-push on lower, and use the same ABI for ImportTS non-optional structs. --- .../Sources/BridgeJSCore/ImportTS.swift | 29 +-- .../Sources/BridgeJSLink/JSGlueGen.swift | 36 ++- .../BridgeJSCodegenTests/Async.swift | 12 +- .../BridgeJSCodegenTests/SwiftClosure.swift | 12 +- .../SwiftStructImports.swift | 14 +- .../__Snapshots__/BridgeJSLinkTests/Async.js | 7 +- .../BridgeJSLinkTests/SwiftClosure.js | 7 +- .../BridgeJSLinkTests/SwiftStructImports.js | 9 +- .../JavaScriptKit/BridgeJSIntrinsics.swift | 13 +- .../BridgeJSRuntimeTests/ExportAPITests.swift | 9 + .../Generated/BridgeJS.swift | 209 +++++++++++++++--- .../Generated/JavaScript/BridgeJS.json | 98 ++++++++ .../JavaScript/ClosureSupportTests.mjs | 18 ++ 13 files changed, 356 insertions(+), 117 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index 474f1a75f..286352915 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -933,19 +933,10 @@ extension BridgeType { case .associatedValueEnum: return LoweringParameterInfo(loweredParameters: [("caseId", .i32)]) case .swiftStruct: - switch context { - case .importTS: - // Swift structs are bridged as JS objects (object IDs) in imported signatures. - return LoweringParameterInfo(loweredParameters: [("objectId", .i32)]) - case .exportSwift: - return LoweringParameterInfo(loweredParameters: []) - } + // `@JS struct` parameters always use the stack ABI (same as arrays/dictionaries). + return LoweringParameterInfo(loweredParameters: []) case .namespaceEnum: throw BridgeJSCoreError("Namespace enums cannot be used as parameters") - case .nullable(.swiftStruct, _) where context == .importTS: - // Optional `@JS struct`s bridge through the stack (isSome discriminator + fields), - // like optional arrays/dictionaries, rather than the non-optional object-id ABI. - return LoweringParameterInfo(loweredParameters: [("isSome", .i32)]) case .nullable(let wrappedType, _): let wrappedInfo = try wrappedType.loweringParameterInfo(context: context) var params = [("isSome", WasmCoreType.i32)] @@ -1005,24 +996,16 @@ extension BridgeType { case .associatedValueEnum: return LiftingReturnInfo(valueToLift: .i32) case .swiftStruct: - switch context { - case .importTS: - // Swift structs are bridged as JS objects (object IDs) in imported signatures. - return LiftingReturnInfo(valueToLift: .i32) - case .exportSwift: - return LiftingReturnInfo(valueToLift: nil) - } + // `@JS struct` returns always use the stack ABI (same as arrays/dictionaries). + return LiftingReturnInfo(valueToLift: nil) case .namespaceEnum: throw BridgeJSCoreError("Namespace enums cannot be used as return values") case .nullable(let wrappedType, _): - // jsObject and `@JS struct` use the stack ABI for optionals — the thunk returns - // void and the value (plus isSome discriminator) flows through the stacks. + // jsObject uses the stack ABI for optionals — the thunk returns void and the + // value (plus isSome discriminator) flows through the stacks. if case .jsObject = wrappedType { return LiftingReturnInfo(valueToLift: nil) } - if case .swiftStruct = wrappedType, context == .importTS { - return LiftingReturnInfo(valueToLift: nil) - } let wrappedInfo = try wrappedType.liftingReturnInfo(context: context) return LiftingReturnInfo(valueToLift: wrappedInfo.valueToLift) case .array, .dictionary: diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index 782988751..1cf0fa298 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -1437,23 +1437,18 @@ struct IntrinsicJSFragment: Sendable { } ) case .swiftStruct(let fullName): - switch context { - case .importTS: - return .jsObjectLiftRetainedObjectId - case .exportSwift: - let base = fullName.replacingOccurrences(of: ".", with: "_") - return IntrinsicJSFragment( - parameters: [], - printCode: { arguments, context in - let (scope, printer) = (context.scope, context.printer) - let resultVar = scope.variable("structValue") - printer.write( - "const \(resultVar) = \(JSGlueVariableScope.reservedStructHelpers).\(base).lift();" - ) - return [resultVar] - } - ) - } + let base = fullName.replacingOccurrences(of: ".", with: "_") + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, context in + let (scope, printer) = (context.scope, context.printer) + let resultVar = scope.variable("structValue") + printer.write( + "const \(resultVar) = \(JSGlueVariableScope.reservedStructHelpers).\(base).lift();" + ) + return [resultVar] + } + ) case .closure: return IntrinsicJSFragment( parameters: ["funcRef"], @@ -1497,12 +1492,7 @@ struct IntrinsicJSFragment: Sendable { case .associatedValueEnum(let fullName): return associatedValueLowerReturn(fullName: fullName) case .swiftStruct(let fullName): - switch context { - case .importTS: - return .jsObjectLowerReturn - case .exportSwift: - return swiftStructLowerReturn(fullName: fullName) - } + return swiftStructLowerReturn(fullName: fullName) case .closure: return IntrinsicJSFragment( parameters: ["value"], diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.swift index 230676e67..f2223ee7c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.swift @@ -507,20 +507,20 @@ func _$Promise_resolve_8JSObjectC(_ promise: JSObject, _ value: JSObject) throws #if arch(wasm32) @_extern(wasm, module: "bjs", name: "promise_resolve_TestModule_10AsyncPointV") -fileprivate func promise_resolve_TestModule_10AsyncPointV_extern(_ promise: Int32, _ value: Int32) -> Void +fileprivate func promise_resolve_TestModule_10AsyncPointV_extern(_ promise: Int32) -> Void #else -fileprivate func promise_resolve_TestModule_10AsyncPointV_extern(_ promise: Int32, _ value: Int32) -> Void { +fileprivate func promise_resolve_TestModule_10AsyncPointV_extern(_ promise: Int32) -> Void { fatalError("Only available on WebAssembly") } #endif -@inline(never) fileprivate func promise_resolve_TestModule_10AsyncPointV(_ promise: Int32, _ value: Int32) -> Void { - return promise_resolve_TestModule_10AsyncPointV_extern(promise, value) +@inline(never) fileprivate func promise_resolve_TestModule_10AsyncPointV(_ promise: Int32) -> Void { + return promise_resolve_TestModule_10AsyncPointV_extern(promise) } func _$Promise_resolve_10AsyncPointV(_ promise: JSObject, _ value: AsyncPoint) throws(JSException) -> Void { - let valueObjectId = value.bridgeJSLowerParameter() + let _ = value.bridgeJSLowerParameter() let promiseValue = promise.bridgeJSLowerParameter() - promise_resolve_TestModule_10AsyncPointV(promiseValue, valueObjectId) + promise_resolve_TestModule_10AsyncPointV(promiseValue) if let error = _swift_js_take_exception() { throw error } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.swift index e1f10ab97..c7ac02fb1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.swift @@ -2684,20 +2684,20 @@ func _$Promise_resolve_SS(_ promise: JSObject, _ value: String) throws(JSExcepti #if arch(wasm32) @_extern(wasm, module: "bjs", name: "promise_resolve_TestModule_6AnimalV") -fileprivate func promise_resolve_TestModule_6AnimalV_extern(_ promise: Int32, _ value: Int32) -> Void +fileprivate func promise_resolve_TestModule_6AnimalV_extern(_ promise: Int32) -> Void #else -fileprivate func promise_resolve_TestModule_6AnimalV_extern(_ promise: Int32, _ value: Int32) -> Void { +fileprivate func promise_resolve_TestModule_6AnimalV_extern(_ promise: Int32) -> Void { fatalError("Only available on WebAssembly") } #endif -@inline(never) fileprivate func promise_resolve_TestModule_6AnimalV(_ promise: Int32, _ value: Int32) -> Void { - return promise_resolve_TestModule_6AnimalV_extern(promise, value) +@inline(never) fileprivate func promise_resolve_TestModule_6AnimalV(_ promise: Int32) -> Void { + return promise_resolve_TestModule_6AnimalV_extern(promise) } func _$Promise_resolve_6AnimalV(_ promise: JSObject, _ value: Animal) throws(JSException) -> Void { - let valueObjectId = value.bridgeJSLowerParameter() + let _ = value.bridgeJSLowerParameter() let promiseValue = promise.bridgeJSLowerParameter() - promise_resolve_TestModule_6AnimalV(promiseValue, valueObjectId) + promise_resolve_TestModule_6AnimalV(promiseValue) if let error = _swift_js_take_exception() { throw error } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStructImports.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStructImports.swift index 0e792ea14..38ec94c0d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStructImports.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftStructImports.swift @@ -48,25 +48,25 @@ fileprivate func _bjs_struct_lift_Point_extern() -> Int32 { #if arch(wasm32) @_extern(wasm, module: "TestModule", name: "bjs_translate") -fileprivate func bjs_translate_extern(_ point: Int32, _ dx: Int32, _ dy: Int32) -> Int32 +fileprivate func bjs_translate_extern(_ dx: Int32, _ dy: Int32) -> Void #else -fileprivate func bjs_translate_extern(_ point: Int32, _ dx: Int32, _ dy: Int32) -> Int32 { +fileprivate func bjs_translate_extern(_ dx: Int32, _ dy: Int32) -> Void { fatalError("Only available on WebAssembly") } #endif -@inline(never) fileprivate func bjs_translate(_ point: Int32, _ dx: Int32, _ dy: Int32) -> Int32 { - return bjs_translate_extern(point, dx, dy) +@inline(never) fileprivate func bjs_translate(_ dx: Int32, _ dy: Int32) -> Void { + return bjs_translate_extern(dx, dy) } func _$translate(_ point: Point, _ dx: Int, _ dy: Int) throws(JSException) -> Point { let dyValue = dy.bridgeJSLowerParameter() let dxValue = dx.bridgeJSLowerParameter() - let pointObjectId = point.bridgeJSLowerParameter() - let ret = bjs_translate(pointObjectId, dxValue, dyValue) + let _ = point.bridgeJSLowerParameter() + bjs_translate(dxValue, dyValue) if let error = _swift_js_take_exception() { throw error } - return Point.bridgeJSLiftReturn(ret) + return Point.bridgeJSLiftReturn() } #if arch(wasm32) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js index 680da9c5e..9f2faf589 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js @@ -280,11 +280,10 @@ export async function createInstantiator(options, swift) { setException(error); } } - bjs["promise_resolve_TestModule_10AsyncPointV"] = function(promise, value) { + bjs["promise_resolve_TestModule_10AsyncPointV"] = function(promise) { try { - const value1 = swift.memory.getObject(value); - swift.memory.release(value); - swift.memory.getObject(promise)[__bjs_promiseSettlers].resolve(value1); + const structValue = structHelpers.AsyncPoint.lift(); + swift.memory.getObject(promise)[__bjs_promiseSettlers].resolve(structValue); } catch (error) { setException(error); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js index f3b9d987c..62c2de8c6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js @@ -345,11 +345,10 @@ export async function createInstantiator(options, swift) { setException(error); } } - bjs["promise_resolve_TestModule_6AnimalV"] = function(promise, value) { + bjs["promise_resolve_TestModule_6AnimalV"] = function(promise) { try { - const value1 = swift.memory.getObject(value); - swift.memory.release(value); - swift.memory.getObject(promise)[__bjs_promiseSettlers].resolve(value1); + const structValue = structHelpers.Animal.lift(); + swift.memory.getObject(promise)[__bjs_promiseSettlers].resolve(structValue); } catch (error) { setException(error); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStructImports.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStructImports.js index 523861b9a..4a2e18d6b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStructImports.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftStructImports.js @@ -223,12 +223,11 @@ export async function createInstantiator(options, swift) { } bjs["swift_js_closure_unregister"] = function(funcRef) {} const TestModule = importObject["TestModule"] = importObject["TestModule"] || {}; - TestModule["bjs_translate"] = function bjs_translate(point, dx, dy) { + TestModule["bjs_translate"] = function bjs_translate(dx, dy) { try { - const value = swift.memory.getObject(point); - swift.memory.release(point); - let ret = imports.translate(value, dx, dy); - return swift.memory.retain(ret); + const structValue = structHelpers.Point.lift(); + let ret = imports.translate(structValue, dx, dy); + structHelpers.Point.lower(ret); } catch (error) { setException(error); } diff --git a/Sources/JavaScriptKit/BridgeJSIntrinsics.swift b/Sources/JavaScriptKit/BridgeJSIntrinsics.swift index a07ca0152..4eeae4dac 100644 --- a/Sources/JavaScriptKit/BridgeJSIntrinsics.swift +++ b/Sources/JavaScriptKit/BridgeJSIntrinsics.swift @@ -1133,13 +1133,12 @@ where StackLiftResult == Self { } extension _BridgedSwiftStruct { - @_spi(BridgeJS) public consuming func bridgeJSLowerParameter() -> Int32 { - return toJSObject().bridgeJSLowerReturn() - } - - @_spi(BridgeJS) public static func bridgeJSLiftReturn(_ objectId: Int32) -> Self { - let jsObject = JSObject.bridgeJSLiftReturn(objectId) - return Self(unsafelyCopying: jsObject) + /// Lower a struct parameter onto the shared stacks for the peer to `lift()`. + /// + /// Same convention as arrays/dictionaries. Use ``toJSObject()`` when a real JS object + /// representation is needed (e.g. `init(unsafelyCopying:)` round-trips). + @_spi(BridgeJS) public consuming func bridgeJSLowerParameter() { + bridgeJSStackPush() } @_spi(BridgeJS) public static func bridgeJSLiftReturn() -> Self { diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 5a852a23d..8778c2c8e 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -1339,6 +1339,15 @@ enum GraphOperations { let noneStr = none.map { "(\($0.dx),\($0.dy))" } ?? "nil" return "\(someStr) | \(noneStr)" } + + /// Swift→JS callback with a struct parameter (ExportSwift stack ABI). + @JS func observeVector(_ callback: (Vector2D) -> Void) { + callback(Vector2D(dx: 1.5, dy: 2.5)) + } + + @JS func mapVector(_ vector: Vector2D, _ callback: (Vector2D) -> Vector2D) -> Vector2D { + return callback(vector) + } } @JS enum NestedStructGroupA { diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 5337e923a..ad6f3fa24 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -455,6 +455,130 @@ public func _invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8JS #endif } +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV") +fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV_extern(_ callback: Int32) -> Void +#else +fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV_extern(_ callback: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV(_ callback: Int32) -> Void { + return invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV_extern(callback) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV") +fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 +#else +fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + return make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV_extern(boxPtr, file, line) +} + +private enum _BJS_Closure_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV { + static func bridgeJSLift(_ callbackId: Int32) -> (Vector2D) -> Vector2D { + let callback = JSObject.bridgeJSLiftParameter(callbackId) + return { [callback] param0 in + #if arch(wasm32) + let _ = param0.bridgeJSLowerParameter() + let callbackValue = callback.bridgeJSLowerParameter() + invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV(callbackValue) + return Vector2D.bridgeJSLiftReturn() + #else + fatalError("Only available on WebAssembly") + #endif + } + } +} + +extension JSTypedClosure where Signature == (Vector2D) -> Vector2D { + init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping (Vector2D) -> Vector2D) { + self.init( + makeClosure: make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV, + body: body, + fileID: fileID, + line: line + ) + } +} + +@_expose(wasm, "invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV") +@_cdecl("invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV") +public func _invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV(_ boxPtr: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let closure = Unmanaged<_BridgeJSTypedClosureBox<(Vector2D) -> Vector2D>>.fromOpaque(boxPtr).takeUnretainedValue().closure + let result = closure(Vector2D.bridgeJSLiftParameter()) + return result.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y") +fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y_extern(_ callback: Int32) -> Void +#else +fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y_extern(_ callback: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y(_ callback: Int32) -> Void { + return invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y_extern(callback) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y") +fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 +#else +fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + return make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y_extern(boxPtr, file, line) +} + +private enum _BJS_Closure_20BridgeJSRuntimeTests8Vector2DV_y { + static func bridgeJSLift(_ callbackId: Int32) -> (Vector2D) -> Void { + let callback = JSObject.bridgeJSLiftParameter(callbackId) + return { [callback] param0 in + #if arch(wasm32) + let _ = param0.bridgeJSLowerParameter() + let callbackValue = callback.bridgeJSLowerParameter() + invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y(callbackValue) + #else + fatalError("Only available on WebAssembly") + #endif + } + } +} + +extension JSTypedClosure where Signature == (Vector2D) -> Void { + init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping (Vector2D) -> Void) { + self.init( + makeClosure: make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y, + body: body, + fileID: fileID, + line: line + ) + } +} + +@_expose(wasm, "invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y") +@_cdecl("invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y") +public func _invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTests8Vector2DV_y(_ boxPtr: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let closure = Unmanaged<_BridgeJSTypedClosureBox<(Vector2D) -> Void>>.fromOpaque(boxPtr).takeUnretainedValue().closure + closure(Vector2D.bridgeJSLiftParameter()) + #else + fatalError("Only available on WebAssembly") + #endif +} + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests9APIResultO_SS") fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTests9APIResultO_SS_extern(_ callback: Int32, _ param0: Int32) -> Int32 @@ -12933,6 +13057,27 @@ public func _bjs_TextProcessor_processOptionalVector(_ _self: UnsafeMutableRawPo #endif } +@_expose(wasm, "bjs_TextProcessor_observeVector") +@_cdecl("bjs_TextProcessor_observeVector") +public func _bjs_TextProcessor_observeVector(_ _self: UnsafeMutableRawPointer, _ callback: Int32) -> Void { + #if arch(wasm32) + TextProcessor.bridgeJSLiftParameter(_self).observeVector(_: _BJS_Closure_20BridgeJSRuntimeTests8Vector2DV_y.bridgeJSLift(callback)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_TextProcessor_mapVector") +@_cdecl("bjs_TextProcessor_mapVector") +public func _bjs_TextProcessor_mapVector(_ _self: UnsafeMutableRawPointer, _ callback: Int32) -> Void { + #if arch(wasm32) + let ret = TextProcessor.bridgeJSLiftParameter(_self).mapVector(_: Vector2D.bridgeJSLiftParameter(), _: _BJS_Closure_20BridgeJSRuntimeTests8Vector2DV_8Vector2DV.bridgeJSLift(callback)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_TextProcessor_deinit") @_cdecl("bjs_TextProcessor_deinit") public func _bjs_TextProcessor_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { @@ -13781,20 +13926,20 @@ func _$Promise_resolve_Sq18AsyncPayloadResultO(_ promise: JSObject, _ value: Opt #if arch(wasm32) @_extern(wasm, module: "bjs", name: "promise_resolve_BridgeJSRuntimeTests_11PublicPointV") -fileprivate func promise_resolve_BridgeJSRuntimeTests_11PublicPointV_extern(_ promise: Int32, _ value: Int32) -> Void +fileprivate func promise_resolve_BridgeJSRuntimeTests_11PublicPointV_extern(_ promise: Int32) -> Void #else -fileprivate func promise_resolve_BridgeJSRuntimeTests_11PublicPointV_extern(_ promise: Int32, _ value: Int32) -> Void { +fileprivate func promise_resolve_BridgeJSRuntimeTests_11PublicPointV_extern(_ promise: Int32) -> Void { fatalError("Only available on WebAssembly") } #endif -@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_11PublicPointV(_ promise: Int32, _ value: Int32) -> Void { - return promise_resolve_BridgeJSRuntimeTests_11PublicPointV_extern(promise, value) +@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_11PublicPointV(_ promise: Int32) -> Void { + return promise_resolve_BridgeJSRuntimeTests_11PublicPointV_extern(promise) } func _$Promise_resolve_11PublicPointV(_ promise: JSObject, _ value: PublicPoint) throws(JSException) -> Void { - let valueObjectId = value.bridgeJSLowerParameter() + let _ = value.bridgeJSLowerParameter() let promiseValue = promise.bridgeJSLowerParameter() - promise_resolve_BridgeJSRuntimeTests_11PublicPointV(promiseValue, valueObjectId) + promise_resolve_BridgeJSRuntimeTests_11PublicPointV(promiseValue) if let error = _swift_js_take_exception() { throw error } } @@ -13802,20 +13947,20 @@ func _$Promise_resolve_11PublicPointV(_ promise: JSObject, _ value: PublicPoint) #if arch(wasm32) @_extern(wasm, module: "bjs", name: "promise_resolve_BridgeJSRuntimeTests_7ContactV") -fileprivate func promise_resolve_BridgeJSRuntimeTests_7ContactV_extern(_ promise: Int32, _ value: Int32) -> Void +fileprivate func promise_resolve_BridgeJSRuntimeTests_7ContactV_extern(_ promise: Int32) -> Void #else -fileprivate func promise_resolve_BridgeJSRuntimeTests_7ContactV_extern(_ promise: Int32, _ value: Int32) -> Void { +fileprivate func promise_resolve_BridgeJSRuntimeTests_7ContactV_extern(_ promise: Int32) -> Void { fatalError("Only available on WebAssembly") } #endif -@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_7ContactV(_ promise: Int32, _ value: Int32) -> Void { - return promise_resolve_BridgeJSRuntimeTests_7ContactV_extern(promise, value) +@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_7ContactV(_ promise: Int32) -> Void { + return promise_resolve_BridgeJSRuntimeTests_7ContactV_extern(promise) } func _$Promise_resolve_7ContactV(_ promise: JSObject, _ value: Contact) throws(JSException) -> Void { - let valueObjectId = value.bridgeJSLowerParameter() + let _ = value.bridgeJSLowerParameter() let promiseValue = promise.bridgeJSLowerParameter() - promise_resolve_BridgeJSRuntimeTests_7ContactV(promiseValue, valueObjectId) + promise_resolve_BridgeJSRuntimeTests_7ContactV(promiseValue) if let error = _swift_js_take_exception() { throw error } } @@ -13886,20 +14031,20 @@ func _$Promise_resolve_SD11PublicPointV(_ promise: JSObject, _ value: [String: P #if arch(wasm32) @_extern(wasm, module: "bjs", name: "promise_resolve_BridgeJSRuntimeTests_9DataPointV") -fileprivate func promise_resolve_BridgeJSRuntimeTests_9DataPointV_extern(_ promise: Int32, _ value: Int32) -> Void +fileprivate func promise_resolve_BridgeJSRuntimeTests_9DataPointV_extern(_ promise: Int32) -> Void #else -fileprivate func promise_resolve_BridgeJSRuntimeTests_9DataPointV_extern(_ promise: Int32, _ value: Int32) -> Void { +fileprivate func promise_resolve_BridgeJSRuntimeTests_9DataPointV_extern(_ promise: Int32) -> Void { fatalError("Only available on WebAssembly") } #endif -@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_9DataPointV(_ promise: Int32, _ value: Int32) -> Void { - return promise_resolve_BridgeJSRuntimeTests_9DataPointV_extern(promise, value) +@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_9DataPointV(_ promise: Int32) -> Void { + return promise_resolve_BridgeJSRuntimeTests_9DataPointV_extern(promise) } func _$Promise_resolve_9DataPointV(_ promise: JSObject, _ value: DataPoint) throws(JSException) -> Void { - let valueObjectId = value.bridgeJSLowerParameter() + let _ = value.bridgeJSLowerParameter() let promiseValue = promise.bridgeJSLowerParameter() - promise_resolve_BridgeJSRuntimeTests_9DataPointV(promiseValue, valueObjectId) + promise_resolve_BridgeJSRuntimeTests_9DataPointV(promiseValue) if let error = _swift_js_take_exception() { throw error } } @@ -14032,14 +14177,14 @@ fileprivate func bjs_AliasImports_jsRoundTripPolygon_static_extern(_ value: Unsa #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_AliasImports_jsRoundTripCoordinate_static") -fileprivate func bjs_AliasImports_jsRoundTripCoordinate_static_extern(_ value: Int32) -> Int32 +fileprivate func bjs_AliasImports_jsRoundTripCoordinate_static_extern() -> Void #else -fileprivate func bjs_AliasImports_jsRoundTripCoordinate_static_extern(_ value: Int32) -> Int32 { +fileprivate func bjs_AliasImports_jsRoundTripCoordinate_static_extern() -> Void { fatalError("Only available on WebAssembly") } #endif -@inline(never) fileprivate func bjs_AliasImports_jsRoundTripCoordinate_static(_ value: Int32) -> Int32 { - return bjs_AliasImports_jsRoundTripCoordinate_static_extern(value) +@inline(never) fileprivate func bjs_AliasImports_jsRoundTripCoordinate_static() -> Void { + return bjs_AliasImports_jsRoundTripCoordinate_static_extern() } #if arch(wasm32) @@ -14117,12 +14262,12 @@ func _$AliasImports_jsRoundTripPolygon(_ value: Polygon) throws(JSException) -> } func _$AliasImports_jsRoundTripCoordinate(_ value: Coordinate) throws(JSException) -> Coordinate { - let valueObjectId = value.bridgeJSLowerParameter() - let ret = bjs_AliasImports_jsRoundTripCoordinate_static(valueObjectId) + let _ = value.bridgeJSLowerParameter() + bjs_AliasImports_jsRoundTripCoordinate_static() if let error = _swift_js_take_exception() { throw error } - return Coordinate.bridgeJSLiftReturn(ret) + return Coordinate.bridgeJSLiftReturn() } func _$AliasImports_jsRoundTripUserId(_ value: UserId) throws(JSException) -> UserId { @@ -16545,25 +16690,25 @@ func _$jsJoinStringThenStackParams(_ s: String, _ a: Optional<[Int]>, _ b: [Int] #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsTranslatePoint") -fileprivate func bjs_jsTranslatePoint_extern(_ point: Int32, _ dx: Int32, _ dy: Int32) -> Int32 +fileprivate func bjs_jsTranslatePoint_extern(_ dx: Int32, _ dy: Int32) -> Void #else -fileprivate func bjs_jsTranslatePoint_extern(_ point: Int32, _ dx: Int32, _ dy: Int32) -> Int32 { +fileprivate func bjs_jsTranslatePoint_extern(_ dx: Int32, _ dy: Int32) -> Void { fatalError("Only available on WebAssembly") } #endif -@inline(never) fileprivate func bjs_jsTranslatePoint(_ point: Int32, _ dx: Int32, _ dy: Int32) -> Int32 { - return bjs_jsTranslatePoint_extern(point, dx, dy) +@inline(never) fileprivate func bjs_jsTranslatePoint(_ dx: Int32, _ dy: Int32) -> Void { + return bjs_jsTranslatePoint_extern(dx, dy) } func _$jsTranslatePoint(_ point: Point, _ dx: Int, _ dy: Int) throws(JSException) -> Point { let dyValue = dy.bridgeJSLowerParameter() let dxValue = dx.bridgeJSLowerParameter() - let pointObjectId = point.bridgeJSLowerParameter() - let ret = bjs_jsTranslatePoint(pointObjectId, dxValue, dyValue) + let _ = point.bridgeJSLowerParameter() + bjs_jsTranslatePoint(dxValue, dyValue) if let error = _swift_js_take_exception() { throw error } - return Point.bridgeJSLiftReturn(ret) + return Point.bridgeJSLiftReturn() } #if arch(wasm32) diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index ac0d9914e..e2a8575e1 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -4697,6 +4697,104 @@ } } + }, + { + "abiName" : "bjs_TextProcessor_observeVector", + "documentation" : "Swift→JS callback with a struct parameter (ExportSwift stack ABI).", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "observeVector", + "parameters" : [ + { + "label" : "_", + "name" : "callback", + "type" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "20BridgeJSRuntimeTests8Vector2DV_y", + "moduleName" : "BridgeJSRuntimeTests", + "parameters" : [ + { + "swiftStruct" : { + "_0" : "Vector2D" + } + } + ], + "returnType" : { + "void" : { + + } + }, + "sendingParameters" : false + }, + "useJSTypedClosure" : false + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_TextProcessor_mapVector", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "mapVector", + "parameters" : [ + { + "label" : "_", + "name" : "vector", + "type" : { + "swiftStruct" : { + "_0" : "Vector2D" + } + } + }, + { + "label" : "_", + "name" : "callback", + "type" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "20BridgeJSRuntimeTests8Vector2DV_8Vector2DV", + "moduleName" : "BridgeJSRuntimeTests", + "parameters" : [ + { + "swiftStruct" : { + "_0" : "Vector2D" + } + } + ], + "returnType" : { + "swiftStruct" : { + "_0" : "Vector2D" + } + }, + "sendingParameters" : false + }, + "useJSTypedClosure" : false + } + } + } + ], + "returnType" : { + "swiftStruct" : { + "_0" : "Vector2D" + } + } } ], "name" : "TextProcessor", diff --git a/Tests/BridgeJSRuntimeTests/JavaScript/ClosureSupportTests.mjs b/Tests/BridgeJSRuntimeTests/JavaScript/ClosureSupportTests.mjs index bab496d09..9461bec1f 100644 --- a/Tests/BridgeJSRuntimeTests/JavaScript/ClosureSupportTests.mjs +++ b/Tests/BridgeJSRuntimeTests/JavaScript/ClosureSupportTests.mjs @@ -366,6 +366,24 @@ export function runJsClosureSupportTests(exports) { ); assert.equal(optVectorResult, "(2.0,4.0) | nil"); + // Swift→JS callback with a struct parameter must deliver the struct fields. + // Regression: empty-stack lift previously yielded `dx`/`dy` as `undefined` + // (and Bool fields wrongly became `true`). + let observed = null; + processor.observeVector((vector) => { + observed = vector; + }); + assert.ok(observed, "observeVector must invoke the callback"); + assert.equal(observed.dx, 1.5); + assert.equal(observed.dy, 2.5); + + const mapped = processor.mapVector({ dx: 3, dy: 4 }, (vector) => ({ + dx: vector.dx * 2, + dy: vector.dy * 2, + })); + assert.equal(mapped.dx, 6); + assert.equal(mapped.dy, 8); + processor.release(); const intToInt = exports.ClosureSupportExports.makeIntToInt(10);