From 549b727240775adcacf2dae0bedafaf5ee36b5e2 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Mon, 4 May 2026 15:39:11 +1000 Subject: [PATCH 1/2] BridgeJS: Fix closures with struct return --- .../Inputs/MacroSwift/SwiftClosure.swift | 11 + .../BridgeJSCodegenTests/SwiftClosure.json | 184 ++++++++++++++++ .../BridgeJSCodegenTests/SwiftClosure.swift | 205 ++++++++++++++++++ .../BridgeJSLinkTests/SwiftClosure.d.ts | 8 + .../BridgeJSLinkTests/SwiftClosure.js | 106 +++++++++ .../JavaScriptKit/BridgeJSIntrinsics.swift | 4 + .../BridgeJSRuntimeTests/ExportAPITests.swift | 12 + .../Generated/BridgeJS.swift | 148 +++++++++++++ .../Generated/JavaScript/BridgeJS.json | 93 ++++++++ .../JavaScript/ClosureSupportTests.mjs | 11 + 10 files changed, 782 insertions(+) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClosure.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClosure.swift index 791b1b7a9..6872d7989 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClosure.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClosure.swift @@ -8,10 +8,21 @@ import JavaScriptKit } } +@JS public struct Animal { + public let type: String + + @JS public init(type: String) { + self.type = type + } +} + @JS class TestProcessor { @JS init(transform: @escaping (String) -> String) {} } +@JS func roundtripAnimal(_ animalClosure: (Animal) -> Animal) -> (Animal) -> Animal +@JS func roundtripOptionalAnimal(_ animalClosure: (Animal?) -> Animal?) -> (Animal?) -> Animal? + @JS func roundtripString(_ stringClosure: (String) -> String) -> (String) -> String @JS func roundtripInt(_ intClosure: (Int) -> Int) -> (Int) -> Int @JS func roundtripBool(_ boolClosure: (Bool) -> Bool) -> (Bool) -> Bool diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.json index abca80150..ac18f6dc2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.json @@ -284,6 +284,152 @@ ], "exposeToGlobal" : false, "functions" : [ + { + "abiName" : "bjs_roundtripAnimal", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundtripAnimal", + "parameters" : [ + { + "label" : "_", + "name" : "animalClosure", + "type" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "10TestModule6AnimalV_6AnimalV", + "moduleName" : "TestModule", + "parameters" : [ + { + "swiftStruct" : { + "_0" : "Animal" + } + } + ], + "returnType" : { + "swiftStruct" : { + "_0" : "Animal" + } + }, + "sendingParameters" : false + }, + "useJSTypedClosure" : false + } + } + } + ], + "returnType" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "10TestModule6AnimalV_6AnimalV", + "moduleName" : "TestModule", + "parameters" : [ + { + "swiftStruct" : { + "_0" : "Animal" + } + } + ], + "returnType" : { + "swiftStruct" : { + "_0" : "Animal" + } + }, + "sendingParameters" : false + }, + "useJSTypedClosure" : false + } + } + }, + { + "abiName" : "bjs_roundtripOptionalAnimal", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundtripOptionalAnimal", + "parameters" : [ + { + "label" : "_", + "name" : "animalClosure", + "type" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "10TestModuleSq6AnimalV_Sq6AnimalV", + "moduleName" : "TestModule", + "parameters" : [ + { + "nullable" : { + "_0" : { + "swiftStruct" : { + "_0" : "Animal" + } + }, + "_1" : "null" + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "swiftStruct" : { + "_0" : "Animal" + } + }, + "_1" : "null" + } + }, + "sendingParameters" : false + }, + "useJSTypedClosure" : false + } + } + } + ], + "returnType" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "10TestModuleSq6AnimalV_Sq6AnimalV", + "moduleName" : "TestModule", + "parameters" : [ + { + "nullable" : { + "_0" : { + "swiftStruct" : { + "_0" : "Animal" + } + }, + "_1" : "null" + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "swiftStruct" : { + "_0" : "Animal" + } + }, + "_1" : "null" + } + }, + "sendingParameters" : false + }, + "useJSTypedClosure" : false + } + } + }, { "abiName" : "bjs_roundtripString", "effects" : { @@ -1872,7 +2018,45 @@ ], "structs" : [ + { + "constructor" : { + "abiName" : "bjs_Animal_init", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "parameters" : [ + { + "label" : "type", + "name" : "type", + "type" : { + "string" : { + } + } + } + ] + }, + "explicitAccessControl" : "public", + "methods" : [ + + ], + "name" : "Animal", + "properties" : [ + { + "isReadonly" : true, + "isStatic" : false, + "name" : "type", + "type" : { + "string" : { + + } + } + } + ], + "swiftCallName" : "Animal" + } ] }, "moduleName" : "TestModule", diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.swift index 348f9a788..4eb7c8da4 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClosure.swift @@ -127,6 +127,69 @@ public func _invoke_swift_closure_TestModule_10TestModule5ThemeO_5ThemeO(_ boxPt #endif } +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "invoke_js_callback_TestModule_10TestModule6AnimalV_6AnimalV") +fileprivate func invoke_js_callback_TestModule_10TestModule6AnimalV_6AnimalV_extern(_ callback: Int32) -> Void +#else +fileprivate func invoke_js_callback_TestModule_10TestModule6AnimalV_6AnimalV_extern(_ callback: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func invoke_js_callback_TestModule_10TestModule6AnimalV_6AnimalV(_ callback: Int32) -> Void { + return invoke_js_callback_TestModule_10TestModule6AnimalV_6AnimalV_extern(callback) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "make_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV") +fileprivate func make_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 +#else +fileprivate func make_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func make_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + return make_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV_extern(boxPtr, file, line) +} + +private enum _BJS_Closure_10TestModule6AnimalV_6AnimalV { + static func bridgeJSLift(_ callbackId: Int32) -> (Animal) -> Animal { + let callback = JSObject.bridgeJSLiftParameter(callbackId) + return { [callback] param0 in + #if arch(wasm32) + let callbackValue = callback.bridgeJSLowerParameter() + let _ = param0.bridgeJSLowerParameter() + invoke_js_callback_TestModule_10TestModule6AnimalV_6AnimalV(callbackValue) + return Animal.bridgeJSLiftReturn() + #else + fatalError("Only available on WebAssembly") + #endif + } + } +} + +extension JSTypedClosure where Signature == (Animal) -> Animal { + init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping (Animal) -> Animal) { + self.init( + makeClosure: make_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV, + body: body, + fileID: fileID, + line: line + ) + } +} + +@_expose(wasm, "invoke_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV") +@_cdecl("invoke_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV") +public func _invoke_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV(_ boxPtr: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let closure = Unmanaged<_BridgeJSTypedClosureBox<(Animal) -> Animal>>.fromOpaque(boxPtr).takeUnretainedValue().closure + let result = closure(Animal.bridgeJSLiftParameter()) + return result.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "invoke_js_callback_TestModule_10TestModule6PersonC_6PersonC") fileprivate func invoke_js_callback_TestModule_10TestModule6PersonC_6PersonC_extern(_ callback: Int32, _ param0: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer @@ -761,6 +824,69 @@ public func _invoke_swift_closure_TestModule_10TestModuleSq5ThemeO_Sq5ThemeO(_ b #endif } +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "invoke_js_callback_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV") +fileprivate func invoke_js_callback_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV_extern(_ callback: Int32, _ param0: Int32) -> Void +#else +fileprivate func invoke_js_callback_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV_extern(_ callback: Int32, _ param0: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func invoke_js_callback_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV(_ callback: Int32, _ param0: Int32) -> Void { + return invoke_js_callback_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV_extern(callback, param0) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "make_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV") +fileprivate func make_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 +#else +fileprivate func make_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func make_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + return make_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV_extern(boxPtr, file, line) +} + +private enum _BJS_Closure_10TestModuleSq6AnimalV_Sq6AnimalV { + static func bridgeJSLift(_ callbackId: Int32) -> (Optional) -> Optional { + let callback = JSObject.bridgeJSLiftParameter(callbackId) + return { [callback] param0 in + #if arch(wasm32) + let callbackValue = callback.bridgeJSLowerParameter() + let param0IsSome = param0.bridgeJSLowerParameter() + invoke_js_callback_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV(callbackValue, param0IsSome) + return Optional.bridgeJSLiftReturn() + #else + fatalError("Only available on WebAssembly") + #endif + } + } +} + +extension JSTypedClosure where Signature == (Optional) -> Optional { + init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping (Optional) -> Optional) { + self.init( + makeClosure: make_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV, + body: body, + fileID: fileID, + line: line + ) + } +} + +@_expose(wasm, "invoke_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV") +@_cdecl("invoke_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV") +public func _invoke_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV(_ boxPtr: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + let closure = Unmanaged<_BridgeJSTypedClosureBox<(Optional) -> Optional>>.fromOpaque(boxPtr).takeUnretainedValue().closure + let result = closure(Optional.bridgeJSLiftParameter()) + return result.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "invoke_js_callback_TestModule_10TestModuleSq6PersonC_Sq6PersonC") fileprivate func invoke_js_callback_TestModule_10TestModuleSq6PersonC_Sq6PersonC_extern(_ callback: Int32, _ param0IsSome: Int32, _ param0Pointer: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer @@ -1358,6 +1484,85 @@ extension APIResult: _BridgedSwiftAssociatedValueEnum { } } +extension Animal: _BridgedSwiftStruct { + @_spi(BridgeJS) @_transparent public static func bridgeJSStackPop() -> Animal { + let type = String.bridgeJSStackPop() + return Animal(type: type) + } + + @_spi(BridgeJS) @_transparent public consuming func bridgeJSStackPush() { + self.type.bridgeJSStackPush() + } + + public init(unsafelyCopying jsObject: JSObject) { + _bjs_struct_lower_Animal(jsObject.bridgeJSLowerParameter()) + self = Self.bridgeJSStackPop() + } + + public func toJSObject() -> JSObject { + let __bjs_self = self + __bjs_self.bridgeJSStackPush() + return JSObject(id: UInt32(bitPattern: _bjs_struct_lift_Animal())) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_Animal") +fileprivate func _bjs_struct_lower_Animal_extern(_ objectId: Int32) -> Void +#else +fileprivate func _bjs_struct_lower_Animal_extern(_ objectId: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lower_Animal(_ objectId: Int32) -> Void { + return _bjs_struct_lower_Animal_extern(objectId) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "swift_js_struct_lift_Animal") +fileprivate func _bjs_struct_lift_Animal_extern() -> Int32 +#else +fileprivate func _bjs_struct_lift_Animal_extern() -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func _bjs_struct_lift_Animal() -> Int32 { + return _bjs_struct_lift_Animal_extern() +} + +@_expose(wasm, "bjs_Animal_init") +@_cdecl("bjs_Animal_init") +public func _bjs_Animal_init(_ typeBytes: Int32, _ typeLength: Int32) -> Void { + #if arch(wasm32) + let ret = Animal(type: String.bridgeJSLiftParameter(typeBytes, typeLength)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_roundtripAnimal") +@_cdecl("bjs_roundtripAnimal") +public func _bjs_roundtripAnimal(_ animalClosure: Int32) -> Int32 { + #if arch(wasm32) + let ret = roundtripAnimal(_: _BJS_Closure_10TestModule6AnimalV_6AnimalV.bridgeJSLift(animalClosure)) + return JSTypedClosure(ret).bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_roundtripOptionalAnimal") +@_cdecl("bjs_roundtripOptionalAnimal") +public func _bjs_roundtripOptionalAnimal(_ animalClosure: Int32) -> Int32 { + #if arch(wasm32) + let ret = roundtripOptionalAnimal(_: _BJS_Closure_10TestModuleSq6AnimalV_Sq6AnimalV.bridgeJSLift(animalClosure)) + return JSTypedClosure(ret).bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_roundtripString") @_cdecl("bjs_roundtripString") public func _bjs_roundtripString(_ stringClosure: Int32) -> Int32 { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.d.ts index ccc95eb3b..a0cbe6c4e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.d.ts @@ -41,6 +41,9 @@ export const APIResultValues: { export type APIResultTag = { tag: typeof APIResultValues.Tag.Success; param0: string } | { tag: typeof APIResultValues.Tag.Failure; param0: number } | { tag: typeof APIResultValues.Tag.Flag; param0: boolean } | { tag: typeof APIResultValues.Tag.Rate; param0: number } | { tag: typeof APIResultValues.Tag.Precise; param0: number } | { tag: typeof APIResultValues.Tag.Info } +export interface Animal { + type: string; +} export type DirectionObject = typeof DirectionValues; export type ThemeObject = typeof ThemeValues; @@ -67,6 +70,8 @@ export type Exports = { TestProcessor: { new(transform: (arg0: string) => string): TestProcessor; } + roundtripAnimal(animalClosure: (arg0: AnimalTag) => AnimalTag): (arg0: AnimalTag) => AnimalTag; + roundtripOptionalAnimal(animalClosure: (arg0: AnimalTag | null) => AnimalTag | null): (arg0: AnimalTag | null) => AnimalTag | null; roundtripString(stringClosure: (arg0: string) => string): (arg0: string) => string; roundtripInt(intClosure: (arg0: number) => number): (arg0: number) => number; roundtripBool(boolClosure: (arg0: boolean) => boolean): (arg0: boolean) => boolean; @@ -92,6 +97,9 @@ export type Exports = { Theme: ThemeObject HttpStatus: HttpStatusObject APIResult: APIResultObject + Animal: { + init(type: string): Animal; + } } export type Imports = { } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js index 03a5504e4..0b4ca3dcc 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.js @@ -85,6 +85,18 @@ export async function createInstantiator(options, swift) { return swift.memory.retain(real); }; + const __bjs_createAnimalHelpers = () => ({ + lower: (value) => { + const bytes = textEncoder.encode(value.type); + const id = swift.memory.retain(bytes); + i32Stack.push(bytes.length); + i32Stack.push(id); + }, + lift: () => { + const string = strStack.pop(); + return { type: string }; + } + }); const __bjs_createAPIResultValuesHelpers = () => ({ lower: (value) => { const enumTag = value.tag; @@ -214,6 +226,13 @@ export async function createInstantiator(options, swift) { bjs["swift_js_pop_i64"] = function() { return i64Stack.pop(); } + bjs["swift_js_struct_lower_Animal"] = function(objectId) { + structHelpers.Animal.lower(swift.memory.getObject(objectId)); + } + bjs["swift_js_struct_lift_Animal"] = function() { + const value = structHelpers.Animal.lift(); + return swift.memory.retain(value); + } bjs["swift_js_return_optional_bool"] = function(isSome, value) { if (isSome === 0) { tmpRetOptionalBool = null; @@ -359,6 +378,31 @@ export async function createInstantiator(options, swift) { }; return makeClosure(boxPtr, file, line, lower_closure_TestModule_10TestModule5ThemeO_5ThemeO); } + bjs["invoke_js_callback_TestModule_10TestModule6AnimalV_6AnimalV"] = function(callbackId) { + try { + const callback = swift.memory.getObject(callbackId); + const structValue = structHelpers.Animal.lift(); + let ret = callback(structValue); + structHelpers.Animal.lower(ret); + } catch (error) { + setException(error); + } + } + bjs["make_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV"] = function(boxPtr, file, line) { + const lower_closure_TestModule_10TestModule6AnimalV_6AnimalV = function(param0) { + structHelpers.Animal.lower(param0); + instance.exports.invoke_swift_closure_TestModule_10TestModule6AnimalV_6AnimalV(boxPtr); + const structValue = structHelpers.Animal.lift(); + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + return structValue; + }; + return makeClosure(boxPtr, file, line, lower_closure_TestModule_10TestModule6AnimalV_6AnimalV); + } bjs["invoke_js_callback_TestModule_10TestModule6PersonC_6PersonC"] = function(callbackId, param0) { try { const callback = swift.memory.getObject(callbackId); @@ -620,6 +664,46 @@ export async function createInstantiator(options, swift) { }; return makeClosure(boxPtr, file, line, lower_closure_TestModule_10TestModuleSq5ThemeO_Sq5ThemeO); } + bjs["invoke_js_callback_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV"] = function(callbackId, param0) { + try { + const callback = swift.memory.getObject(callbackId); + let optResult; + if (param0) { + const struct = structHelpers.Animal.lift(); + optResult = struct; + } else { + optResult = null; + } + let ret = callback(optResult); + const isSome = ret != null; + if (isSome) { + structHelpers.Animal.lower(ret); + } + i32Stack.push(isSome ? 1 : 0); + } catch (error) { + setException(error); + } + } + bjs["make_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV"] = function(boxPtr, file, line) { + const lower_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV = function(param0) { + const isSome = param0 != null; + if (isSome) { + structHelpers.Animal.lower(param0); + } + i32Stack.push(+isSome); + instance.exports.invoke_swift_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV(boxPtr); + const isSome1 = i32Stack.pop(); + const optResult = isSome1 ? structHelpers.Animal.lift() : null; + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + return optResult; + }; + return makeClosure(boxPtr, file, line, lower_closure_TestModule_10TestModuleSq6AnimalV_Sq6AnimalV); + } bjs["invoke_js_callback_TestModule_10TestModuleSq6PersonC_Sq6PersonC"] = function(callbackId, param0IsSome, param0Pointer) { try { const callback = swift.memory.getObject(callbackId); @@ -971,12 +1055,25 @@ export async function createInstantiator(options, swift) { return TestProcessor.__construct(ret); } } + const AnimalHelpers = __bjs_createAnimalHelpers(); + structHelpers.Animal = AnimalHelpers; + const APIResultHelpers = __bjs_createAPIResultValuesHelpers(); enumHelpers.APIResult = APIResultHelpers; const exports = { Person, TestProcessor, + roundtripAnimal: function bjs_roundtripAnimal(animalClosure) { + const callbackId = swift.memory.retain(animalClosure); + const ret = instance.exports.bjs_roundtripAnimal(callbackId); + return swift.memory.getObject(ret); + }, + roundtripOptionalAnimal: function bjs_roundtripOptionalAnimal(animalClosure) { + const callbackId = swift.memory.retain(animalClosure); + const ret = instance.exports.bjs_roundtripOptionalAnimal(callbackId); + return swift.memory.getObject(ret); + }, roundtripString: function bjs_roundtripString(stringClosure) { const callbackId = swift.memory.retain(stringClosure); const ret = instance.exports.bjs_roundtripString(callbackId); @@ -1086,6 +1183,15 @@ export async function createInstantiator(options, swift) { Theme: ThemeValues, HttpStatus: HttpStatusValues, APIResult: APIResultValues, + Animal: { + init: function(type) { + const typeBytes = textEncoder.encode(type); + const typeId = swift.memory.retain(typeBytes); + instance.exports.bjs_Animal_init(typeId, typeBytes.length); + const structValue = structHelpers.Animal.lift(); + return structValue; + }, + }, }; _exports = exports; return exports; diff --git a/Sources/JavaScriptKit/BridgeJSIntrinsics.swift b/Sources/JavaScriptKit/BridgeJSIntrinsics.swift index 867d0e835..2a18401b4 100644 --- a/Sources/JavaScriptKit/BridgeJSIntrinsics.swift +++ b/Sources/JavaScriptKit/BridgeJSIntrinsics.swift @@ -847,6 +847,10 @@ extension _BridgedSwiftStruct { return Self(unsafelyCopying: jsObject) } + @_spi(BridgeJS) public static func bridgeJSLiftReturn() -> Self { + bridgeJSStackPop() + } + @_spi(BridgeJS) public static func bridgeJSLiftParameter() -> Self { bridgeJSStackPop() } diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 2562fe18e..c81622f0d 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -1285,6 +1285,18 @@ enum GraphOperations { processor.increment(by: 7) return callback(processor) + " | " + callback(nil) } + + @JS func processVector(_ callback: (Double) -> Vector2D) -> Double { + return callback(3.0).magnitude() + } + + @JS func processOptionalVector(_ callback: (Double) -> Vector2D?) -> String { + let some = callback(2.0) + let none = callback(-1.0) + let someStr = some.map { "(\($0.dx),\($0.dy))" } ?? "nil" + let noneStr = none.map { "(\($0.dx),\($0.dy))" } ?? "nil" + return "\(someStr) | \(noneStr)" + } } class ExportAPITests: XCTestCase { diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 4d920873d..771838716 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -776,6 +776,69 @@ public func _invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSS_ #endif } +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV") +fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV_extern(_ callback: Int32, _ param0: Float64) -> Void +#else +fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV_extern(_ callback: Int32, _ param0: Float64) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV(_ callback: Int32, _ param0: Float64) -> Void { + return invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV_extern(callback, param0) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV") +fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 +#else +fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + return make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV_extern(boxPtr, file, line) +} + +private enum _BJS_Closure_20BridgeJSRuntimeTestsSd_8Vector2DV { + static func bridgeJSLift(_ callbackId: Int32) -> (Double) -> Vector2D { + let callback = JSObject.bridgeJSLiftParameter(callbackId) + return { [callback] param0 in + #if arch(wasm32) + let callbackValue = callback.bridgeJSLowerParameter() + let param0Value = param0.bridgeJSLowerParameter() + invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV(callbackValue, param0Value) + return Vector2D.bridgeJSLiftReturn() + #else + fatalError("Only available on WebAssembly") + #endif + } + } +} + +extension JSTypedClosure where Signature == (Double) -> Vector2D { + init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping (Double) -> Vector2D) { + self.init( + makeClosure: make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV, + body: body, + fileID: fileID, + line: line + ) + } +} + +@_expose(wasm, "invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV") +@_cdecl("invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV") +public func _invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_8Vector2DV(_ boxPtr: UnsafeMutableRawPointer, _ param0: Float64) -> Void { + #if arch(wasm32) + let closure = Unmanaged<_BridgeJSTypedClosureBox<(Double) -> Vector2D>>.fromOpaque(boxPtr).takeUnretainedValue().closure + let result = closure(Double.bridgeJSLiftParameter(param0)) + return result.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sd") fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sd_extern(_ callback: Int32, _ param0: Float64) -> Float64 @@ -839,6 +902,69 @@ public func _invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_ #endif } +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV") +fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV_extern(_ callback: Int32, _ param0: Float64) -> Void +#else +fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV_extern(_ callback: Int32, _ param0: Float64) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV(_ callback: Int32, _ param0: Float64) -> Void { + return invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV_extern(callback, param0) +} + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV") +fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 +#else +fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV_extern(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV(_ boxPtr: UnsafeMutableRawPointer, _ file: UnsafePointer, _ line: UInt32) -> Int32 { + return make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV_extern(boxPtr, file, line) +} + +private enum _BJS_Closure_20BridgeJSRuntimeTestsSd_Sq8Vector2DV { + static func bridgeJSLift(_ callbackId: Int32) -> (Double) -> Optional { + let callback = JSObject.bridgeJSLiftParameter(callbackId) + return { [callback] param0 in + #if arch(wasm32) + let callbackValue = callback.bridgeJSLowerParameter() + let param0Value = param0.bridgeJSLowerParameter() + invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV(callbackValue, param0Value) + return Optional.bridgeJSLiftReturn() + #else + fatalError("Only available on WebAssembly") + #endif + } + } +} + +extension JSTypedClosure where Signature == (Double) -> Optional { + init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping (Double) -> Optional) { + self.init( + makeClosure: make_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV, + body: body, + fileID: fileID, + line: line + ) + } +} + +@_expose(wasm, "invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV") +@_cdecl("invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV") +public func _invoke_swift_closure_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSd_Sq8Vector2DV(_ boxPtr: UnsafeMutableRawPointer, _ param0: Float64) -> Void { + #if arch(wasm32) + let closure = Unmanaged<_BridgeJSTypedClosureBox<(Double) -> Optional>>.fromOpaque(boxPtr).takeUnretainedValue().closure + let result = closure(Double.bridgeJSLiftParameter(param0)) + return result.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSiSSSd_SS") fileprivate func invoke_js_callback_BridgeJSRuntimeTests_20BridgeJSRuntimeTestsSiSSSd_SS_extern(_ callback: Int32, _ param0: Int32, _ param1Bytes: Int32, _ param1Length: Int32, _ param2: Float64) -> Int32 @@ -10442,6 +10568,28 @@ public func _bjs_TextProcessor_processOptionalDataProcessor(_ _self: UnsafeMutab #endif } +@_expose(wasm, "bjs_TextProcessor_processVector") +@_cdecl("bjs_TextProcessor_processVector") +public func _bjs_TextProcessor_processVector(_ _self: UnsafeMutableRawPointer, _ callback: Int32) -> Float64 { + #if arch(wasm32) + let ret = TextProcessor.bridgeJSLiftParameter(_self).processVector(_: _BJS_Closure_20BridgeJSRuntimeTestsSd_8Vector2DV.bridgeJSLift(callback)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_TextProcessor_processOptionalVector") +@_cdecl("bjs_TextProcessor_processOptionalVector") +public func _bjs_TextProcessor_processOptionalVector(_ _self: UnsafeMutableRawPointer, _ callback: Int32) -> Void { + #if arch(wasm32) + let ret = TextProcessor.bridgeJSLiftParameter(_self).processOptionalVector(_: _BJS_Closure_20BridgeJSRuntimeTestsSd_Sq8Vector2DV.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 { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index d9108f2e7..7a3e7f7fe 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -4057,6 +4057,99 @@ "returnType" : { "string" : { + } + } + }, + { + "abiName" : "bjs_TextProcessor_processVector", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "processVector", + "parameters" : [ + { + "label" : "_", + "name" : "callback", + "type" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "20BridgeJSRuntimeTestsSd_8Vector2DV", + "moduleName" : "BridgeJSRuntimeTests", + "parameters" : [ + { + "double" : { + + } + } + ], + "returnType" : { + "swiftStruct" : { + "_0" : "Vector2D" + } + }, + "sendingParameters" : false + }, + "useJSTypedClosure" : false + } + } + } + ], + "returnType" : { + "double" : { + + } + } + }, + { + "abiName" : "bjs_TextProcessor_processOptionalVector", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "processOptionalVector", + "parameters" : [ + { + "label" : "_", + "name" : "callback", + "type" : { + "closure" : { + "_0" : { + "isAsync" : false, + "isThrows" : false, + "mangleName" : "20BridgeJSRuntimeTestsSd_Sq8Vector2DV", + "moduleName" : "BridgeJSRuntimeTests", + "parameters" : [ + { + "double" : { + + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "swiftStruct" : { + "_0" : "Vector2D" + } + }, + "_1" : "null" + } + }, + "sendingParameters" : false + }, + "useJSTypedClosure" : false + } + } + } + ], + "returnType" : { + "string" : { + } } } diff --git a/Tests/BridgeJSRuntimeTests/JavaScript/ClosureSupportTests.mjs b/Tests/BridgeJSRuntimeTests/JavaScript/ClosureSupportTests.mjs index ebe00548b..bab496d09 100644 --- a/Tests/BridgeJSRuntimeTests/JavaScript/ClosureSupportTests.mjs +++ b/Tests/BridgeJSRuntimeTests/JavaScript/ClosureSupportTests.mjs @@ -355,6 +355,17 @@ export function runJsClosureSupportTests(exports) { }); assert.equal(optDpResult, "DP: 7 | DP: null"); + const vectorMagnitude = processor.processVector((value) => ({ + dx: value, + dy: 4.0, + })); + assert.equal(vectorMagnitude, 5.0); + + const optVectorResult = processor.processOptionalVector((value) => + value > 0 ? { dx: value, dy: value * 2 } : null, + ); + assert.equal(optVectorResult, "(2.0,4.0) | nil"); + processor.release(); const intToInt = exports.ClosureSupportExports.makeIntToInt(10); From 4326481aacc61943b10fe5efaa2227b625ffa683 Mon Sep 17 00:00:00 2001 From: William Taylor Date: Tue, 5 May 2026 10:13:50 +1000 Subject: [PATCH 2/2] BridgeJS: Fix TypeScript type definition generation for struct arguments --- Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift | 4 +--- .../__Snapshots__/BridgeJSLinkTests/SwiftClosure.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 64a0d2394..ceaaae8f1 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -1441,8 +1441,6 @@ public struct BridgeJSLink { } } return type.tsType - case .swiftStruct(let name): - return name.components(separatedBy: ".").last ?? name case .nullable(let wrapped, let kind): let base = resolveTypeScriptType(wrapped, exportedSkeletons: exportedSkeletons) return "\(base) | \(kind.absenceLiteral)" @@ -3612,7 +3610,7 @@ extension BridgeType { case .associatedValueEnum(let name): return "\(name)Tag" case .swiftStruct(let name): - return "\(name)Tag" + return name.components(separatedBy: ".").last ?? name case .namespaceEnum(let name): return name case .swiftProtocol(let name): diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.d.ts index a0cbe6c4e..d024be7dd 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClosure.d.ts @@ -70,8 +70,8 @@ export type Exports = { TestProcessor: { new(transform: (arg0: string) => string): TestProcessor; } - roundtripAnimal(animalClosure: (arg0: AnimalTag) => AnimalTag): (arg0: AnimalTag) => AnimalTag; - roundtripOptionalAnimal(animalClosure: (arg0: AnimalTag | null) => AnimalTag | null): (arg0: AnimalTag | null) => AnimalTag | null; + roundtripAnimal(animalClosure: (arg0: Animal) => Animal): (arg0: Animal) => Animal; + roundtripOptionalAnimal(animalClosure: (arg0: Animal | null) => Animal | null): (arg0: Animal | null) => Animal | null; roundtripString(stringClosure: (arg0: string) => string): (arg0: string) => string; roundtripInt(intClosure: (arg0: number) => number): (arg0: number) => number; roundtripBool(boolClosure: (arg0: boolean) => boolean): (arg0: boolean) => boolean;